Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rule require-throws #574

Merged
merged 7 commits into from
Jun 17, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import requireReturns from './rules/requireReturns';
import requireReturnsCheck from './rules/requireReturnsCheck';
import requireReturnsDescription from './rules/requireReturnsDescription';
import requireReturnsType from './rules/requireReturnsType';
import requireThrows from './rules/requireThrows';
import validTypes from './rules/validTypes';
import requireJsdoc from './rules/requireJsdoc';

Expand Down Expand Up @@ -119,6 +120,7 @@ export default {
'require-returns-check': requireReturnsCheck,
'require-returns-description': requireReturnsDescription,
'require-returns-type': requireReturnsType,
'require-throws': requireThrows,
'valid-types': validTypes,
},
};
4 changes: 4 additions & 0 deletions src/iterateJsdoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,10 @@ const getUtils = (
return jsdocUtils.hasReturnValue(node);
};

utils.hasThrowValue = () => {
return jsdocUtils.hasThrowValue(node);
};

utils.isAsync = () => {
return node.async;
};
Expand Down
56 changes: 56 additions & 0 deletions src/jsdocUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,61 @@ const hasReturnValue = (node) => {
}
};

/**
* Checks if a node has a throws statement.
*
* @param {object} node
* @returns {boolean}
*/
const hasThrowValue = (node) => {
if (!node) {
return false;
}
switch (node.type) {
case 'FunctionExpression':
case 'FunctionDeclaration':
case 'ArrowFunctionExpression': {
return node.expression || hasThrowValue(node.body);
}
case 'BlockStatement': {
return node.body.some((bodyNode) => {
return bodyNode.type !== 'FunctionDeclaration' && hasThrowValue(bodyNode);
});
}
case 'WhileStatement':
case 'DoWhileStatement':
case 'ForStatement':
case 'ForInStatement':
case 'ForOfStatement':
case 'WithStatement': {
return hasThrowValue(node.body);
}
case 'IfStatement': {
return hasThrowValue(node.consequent) || hasThrowValue(node.alternate);
}

// We only consider it to throw an error if the catch or finally blocks throw an error.
case 'TryStatement': {
return hasThrowValue(node.handler && node.handler.body) ||
hasThrowValue(node.finalizer);
}
case 'SwitchStatement': {
return node.cases.some(
(someCase) => {
return someCase.consequent.some(hasThrowValue);
},
);
}
case 'ThrowStatement': {
return true;
}

default: {
return false;
}
}
};

/** @param {string} tag */
/*
const isInlineTag = (tag) => {
Expand Down Expand Up @@ -668,6 +723,7 @@ export default {
hasDefinedTypeReturnTag,
hasReturnValue,
hasTag,
hasThrowValue,
isNamepathDefiningTag,
isValidTag,
parseClosureTemplateTag,
Expand Down
82 changes: 82 additions & 0 deletions src/rules/requireThrows.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import iterateJsdoc from '../iterateJsdoc';

/**
* We can skip checking for a throws value, in case the documentation is inherited
* or the method is either a constructor or an abstract method.
*
* @param {*} utils
* a reference to the utils which are used to probe if a tag is present or not.
* @returns {boolean}
* true in case deep checking can be skipped; otherwise false.
*/
const canSkip = (utils) => {
return utils.hasATag([
// inheritdoc implies that all documentation is inherited
// see https://jsdoc.app/tags-inheritdoc.html
//
// Abstract methods are by definition incomplete,
// so it is not necessary to document that they throw an error.
'abstract',
'virtual',
]) ||
utils.avoidDocs();
};

export default iterateJsdoc(({
report,
utils,
}) => {
// A preflight check. We do not need to run a deep check for abstract functions.
if (canSkip(utils)) {
return;
}

const tagName = utils.getPreferredTagName({tagName: 'throws'});
if (!tagName) {
return;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to cover this case in a test?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be enough to add a (passing) example which doesn't have @throws at all (or where it has @throws but settings.jsdoc.tagNamePreference is set to map throws to something else like throw and if @throw wasn't present).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only line that I can't figure out how to cover. I feel like it should be covered by this test, but it seems like I'm not understanding how utils.getPreferredTagName() is working?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, I've added the test (it was setting throws to false on tagNamePreference settings, i.e., if someone had indicated they wished to block the throws tag yet were still using this rule. :)

I think it looked good, but need a little more time to give it another look over before merging.

}

const tags = utils.getTags(tagName);
const iteratingFunction = utils.isIteratingFunction();

// In case the code returns something, we expect a return value in JSDoc.
const [tag] = tags;
const missingThrowsTag = typeof tag === 'undefined' || tag === null;

const shouldReport = () => {
if (!missingThrowsTag) {
return false;
}

return iteratingFunction && utils.hasThrowValue();
};

if (shouldReport()) {
report(`Missing JSDoc @${tagName} declaration.`);
}
}, {
contextDefaults: true,
meta: {
schema: [
{
additionalProperties: false,
properties: {
contexts: {
items: {
type: 'string',
},
type: 'array',
},
exemptedBy: {
items: {
type: 'string',
},
type: 'array',
},
},
type: 'object',
},
],
type: 'suggestion',
},
});
204 changes: 204 additions & 0 deletions test/rules/assertions/requireThrows.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
export default {
invalid: [
{
code: `
/**
*
*/
function quux (foo) {
throw new Error('err')
}
`,
errors: [
{
line: 2,
message: 'Missing JSDoc @throws declaration.',
},
],
},
{
code: `
/**
*
*/
function quux (foo) {
while(true) {
throw new Error('err')
}
}
`,
errors: [
{
line: 2,
message: 'Missing JSDoc @throws declaration.',
},
],
},
{
code: `
/**
*
*/
function quux (foo) {
if (true) {
throw new Error('err')
}
}
`,
errors: [
{
line: 2,
message: 'Missing JSDoc @throws declaration.',
},
],
},
{
code: `
/**
*
*/
function quux (foo) {
if (false) {
// do nothing
} else {
throw new Error('err')
}
}
`,
errors: [
{
line: 2,
message: 'Missing JSDoc @throws declaration.',
},
],
},
{
code: `
/**
*
*/
function quux (foo) {
try {
throw new Error('err')
} catch(e) {
throw new Error(e.message)
}
}
`,
errors: [
{
line: 2,
message: 'Missing JSDoc @throws declaration.',
},
],
},
{
code: `
/**
*
*/
function quux (foo) {
try {
// do nothing
} finally {
throw new Error(e.message)
}
}
`,
errors: [
{
line: 2,
message: 'Missing JSDoc @throws declaration.',
},
],
},
{
code: `
/**
*
*/
function quux (foo) {
const a = 'b'
switch(a) {
case 'b':
throw new Error('err')
}
}
`,
errors: [
{
line: 2,
message: 'Missing JSDoc @throws declaration.',
},
],
},
],
valid: [
{
code: `
/**
* @throws An error.
*/
function quux () {
throw new Error('err')
}
`,
},
{
code: `
/**
*
*/
function quux (foo) {
try {
throw new Error('err')
} catch(e) {}
}
`,
},
{
code: `
/**
* @inheritdoc
*/
function quux (foo) {
throw new Error('err')
}
`,
},
{
code: `
/**
* @abstract
*/
function quux (foo) {
throw new Error('err')
}
`,
},
{
code: `
/**
*
*/
function quux (foo) {
}
`,
},
{
code: `
/**
* @type {MyCallback}
*/
function quux () {
throw new Error('err')
}
`,
options: [
{
exemptedBy: ['type'],
},
],
},
],
};
1 change: 1 addition & 0 deletions test/rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const ruleTester = new RuleTester();
'require-returns-check',
'require-returns-description',
'require-returns-type',
'require-throws',
'valid-types',
]).forEach((ruleName) => {
const rule = config.rules[ruleName];
Expand Down