Skip to content

Commit

Permalink
feat(require-file-overview): add rule for every file to have a `fil…
Browse files Browse the repository at this point in the history
…e` tag; fixes #55
  • Loading branch information
brettz9 committed Dec 31, 2019
1 parent 1a2bb7f commit 7839625
Show file tree
Hide file tree
Showing 9 changed files with 428 additions and 26 deletions.
1 change: 1 addition & 0 deletions .README/README.md
Expand Up @@ -345,6 +345,7 @@ only (e.g., to match `Array` if the type is `Array` vs. `Array.<string>`).
{"gitdown": "include", "file": "./rules/require-description-complete-sentence.md"}
{"gitdown": "include", "file": "./rules/require-description.md"}
{"gitdown": "include", "file": "./rules/require-example.md"}
{"gitdown": "include", "file": "./rules/require-file-overview.md"}
{"gitdown": "include", "file": "./rules/require-hyphen-before-param-description.md"}
{"gitdown": "include", "file": "./rules/require-jsdoc.md"}
{"gitdown": "include", "file": "./rules/require-param-description.md"}
Expand Down
18 changes: 18 additions & 0 deletions .README/rules/require-file-overview.md
@@ -0,0 +1,18 @@
### `require-file-overview`

Requires that all files have a `@file`, `@fileoverview`, or `@overview` tag.

#### Fixer

The fixer for `require-example` will add an empty `@file`. Note that if you
need to report a missing description for `file`, you can add `file` on
the `tags` option with `match-description` (and the `contexts` option
set to "any").

|||
|---|---|
|Context|Everywhere|
|Tags|`file`|
|Aliases|`fileoverview`, `overview`|

<!-- assertions requireFileOverview -->
92 changes: 92 additions & 0 deletions README.md
Expand Up @@ -37,6 +37,7 @@ JSDoc linting rules for ESLint.
* [`require-description-complete-sentence`](#eslint-plugin-jsdoc-rules-require-description-complete-sentence)
* [`require-description`](#eslint-plugin-jsdoc-rules-require-description)
* [`require-example`](#eslint-plugin-jsdoc-rules-require-example)
* [`require-file-overview`](#eslint-plugin-jsdoc-rules-require-file-overview)
* [`require-hyphen-before-param-description`](#eslint-plugin-jsdoc-rules-require-hyphen-before-param-description)
* [`require-jsdoc`](#eslint-plugin-jsdoc-rules-require-jsdoc)
* [`require-param-description`](#eslint-plugin-jsdoc-rules-require-param-description)
Expand Down Expand Up @@ -6193,6 +6194,97 @@ function quux () {
````
<a name="eslint-plugin-jsdoc-rules-require-file-overview"></a>
### <code>require-file-overview</code>
Requires that all files have a `@file`, `@fileoverview`, or `@overview` tag.
<a name="eslint-plugin-jsdoc-rules-require-file-overview-fixer-1"></a>
#### Fixer
The fixer for `require-example` will add an empty `@file`. Note that if you
need to report a missing description for `file`, you can add `file` on
the `tags` option with `match-description` (and the `contexts` option
set to "any").
|||
|---|---|
|Context|Everywhere|
|Tags|`file`|
|Aliases|`fileoverview`, `overview`|
The following patterns are considered problems:
````js
// Message: Missing @file
/**
*
*/
// Message: Missing @file
/**
*
*/
function quux () {}
// Message: Missing @file
/**
*
*/
function quux () {}
// Settings: {"jsdoc":{"tagNamePreference":{"file":"fileoverview"}}}
// Message: Missing @fileoverview
/**
*
*/
function quux () {}
// Settings: {"jsdoc":{"tagNamePreference":{"file":"overview"}}}
// Message: Missing @overview
/**
* @param a
*/
function quux (a) {}
// Message: Missing @file
/**
* @param a
*/
function quux (a) {}
/**
* @param b
*/
function bar (b) {}
// Message: Missing @file
````
The following patterns are not considered problems:
````js
/**
* @file
*/
/**
* @fileoverview
*/
// Settings: {"jsdoc":{"tagNamePreference":{"file":"fileoverview"}}}
/**
* @overview
*/
// Settings: {"jsdoc":{"tagNamePreference":{"file":"overview"}}}
/**
* @file Description of file
*/
````
<a name="eslint-plugin-jsdoc-rules-require-hyphen-before-param-description"></a>
### <code>require-hyphen-before-param-description</code>
Expand Down
3 changes: 3 additions & 0 deletions src/index.js
Expand Up @@ -17,6 +17,7 @@ import noUndefinedTypes from './rules/noUndefinedTypes';
import requireDescriptionCompleteSentence from './rules/requireDescriptionCompleteSentence';
import requireDescription from './rules/requireDescription';
import requireExample from './rules/requireExample';
import requireFileOverview from './rules/requireFileOverview';
import requireHyphenBeforeParamDescription from './rules/requireHyphenBeforeParamDescription';
import requireParamName from './rules/requireParamName';
import requireParam from './rules/requireParam';
Expand Down Expand Up @@ -52,6 +53,7 @@ export default {
'jsdoc/require-description': 'off',
'jsdoc/require-description-complete-sentence': 'off',
'jsdoc/require-example': 'off',
'jsdoc/require-file-overview': 'off',
'jsdoc/require-hyphen-before-param-description': 'off',
'jsdoc/require-jsdoc': 'warn',
'jsdoc/require-param': 'warn',
Expand Down Expand Up @@ -85,6 +87,7 @@ export default {
'require-description': requireDescription,
'require-description-complete-sentence': requireDescriptionCompleteSentence,
'require-example': requireExample,
'require-file-overview': requireFileOverview,
'require-hyphen-before-param-description': requireHyphenBeforeParamDescription,
'require-jsdoc': requireJsdoc,
'require-param': requireParam,
Expand Down
83 changes: 58 additions & 25 deletions src/iterateJsdoc.js
Expand Up @@ -74,26 +74,58 @@ const parseComment = (commentNode, indent, trim = true) => {
})[0] || {};
};

const getBasicUtils = (context, {tagNamePreference, mode}) => {
const utils = {};
utils.reportSettings = (message) => {
context.report({
loc: {
start: {
column: 1,
line: 1,
},
},
message,
});
};

utils.getPreferredTagNameObject = ({tagName}) => {
const ret = jsdocUtils.getPreferredTagName(context, mode, tagName, tagNamePreference);
const isObject = ret && typeof ret === 'object';
if (ret === false || isObject && !ret.replacement) {
return {
blocked: true,
tagName,
};
}

return ret;
};

return utils;
};

const getUtils = (
node,
jsdoc,
jsdocNode,
{
settings,
report,
context,
) => {
const ancestors = context.getAncestors();
const sourceCode = context.getSourceCode();

const utils = getBasicUtils(context, settings);

const {
tagNamePreference,
overrideReplacesDocs,
implementsReplacesDocs,
augmentsExtendsReplacesDocs,
maxLines,
minLines,
mode,
},
report,
context,
) => {
const ancestors = context.getAncestors();
const sourceCode = context.getSourceCode();

const utils = {};
} = settings;

utils.stringify = (tagBlock) => {
const indent = jsdocUtils.getIndent(sourceCode);
Expand Down Expand Up @@ -314,18 +346,6 @@ const getUtils = (
});
};

utils.reportSettings = (message) => {
context.report({
loc: {
start: {
column: 1,
line: 1,
},
},
message,
});
};

return utils;
};

Expand Down Expand Up @@ -410,7 +430,8 @@ const makeReport = (context, commentNode) => {
*/

const iterate = (
ruleConfig, context, lines, jsdocNode, node, settings, sourceCode, iterator,
ruleConfig, context, lines, jsdocNode, node, settings,
sourceCode, iterator, state, exit,
) => {
const sourceLine = lines[jsdocNode.loc.start.line - 1];
const indent = sourceLine.charAt(0).repeat(jsdocNode.loc.start.column);
Expand All @@ -436,13 +457,15 @@ const iterate = (

iterator({
context,
exit,
indent,
jsdoc,
jsdocNode,
node,
report,
settings,
sourceCode,
state,
utils,
});
};
Expand All @@ -457,25 +480,35 @@ const iterate = (
const iterateAllJsdocs = (iterator, ruleConfig) => {
const trackedJsdocs = [];

const callIterator = (context, node, jsdocNodes) => {
const callIterator = (context, node, jsdocNodes, state, lastCall) => {
const sourceCode = context.getSourceCode();
const settings = getSettings(context);
const {lines} = sourceCode;

const utils = getBasicUtils(context, settings);
jsdocNodes.forEach((jsdocNode) => {
if (!(/^\/\*\*\s/).test(sourceCode.getText(jsdocNode))) {
return;
}
iterate(
ruleConfig, context, lines, jsdocNode, node,
settings, sourceCode, iterator,
state,
);
});
if (lastCall && ruleConfig.exit) {
ruleConfig.exit({
state,
utils,
});
}
};

return {
create (context) {
const sourceCode = context.getSourceCode();
const settings = getSettings(context);
const state = {};

return {
'*:not(Program)' (node) {
Expand All @@ -491,15 +524,15 @@ const iterateAllJsdocs = (iterator, ruleConfig) => {
}

trackedJsdocs.push(comment);
callIterator(context, node, [comment]);
callIterator(context, node, [comment], state);
},
'Program:exit' () {
const allJsdocs = sourceCode.getAllComments();
const untrackedJSdoc = allJsdocs.filter((node) => {
return !trackedJsdocs.includes(node);
});

callIterator(context, null, untrackedJSdoc);
callIterator(context, null, untrackedJSdoc, state, true);
},
};
},
Expand Down
2 changes: 1 addition & 1 deletion src/jsdocUtils.js
Expand Up @@ -97,7 +97,7 @@ const getPreferredTagName = (
mode : ParserMode,
name : string,
tagPreference : Object = {},
) : string => {
) : string|Object => {
const prefValues = _.values(tagPreference);
if (prefValues.includes(name) || prefValues.some((prefVal) => {
return prefVal && typeof prefVal === 'object' && prefVal.replacement === name;
Expand Down
35 changes: 35 additions & 0 deletions src/rules/requireFileOverview.js
@@ -0,0 +1,35 @@
import iterateJsdoc from '../iterateJsdoc';

export default iterateJsdoc(({
state,
utils,
}) => {
if (state.hasFileOverview) {
return;
}
const targetTagName = utils.getPreferredTagName({tagName: 'file'});

// Can we somehow prevent repeating execution of this non-exiting
// iterator instead of repeatedly checking the value above?
state.hasFileOverview = targetTagName && utils.hasTag(targetTagName);
}, {
exit ({state, utils}) {
if (state.hasFileOverview) {
return;
}
const obj = utils.getPreferredTagNameObject({tagName: 'file'});
if (obj && obj.blocked) {
utils.reportSettings(
`\`settings.jsdoc.tagNamePreference\` cannot block @${obj.tagName} ` +
'for the `require-file-overview` rule',
);
} else {
utils.reportSettings(`Missing @${obj && obj.replacement || obj}`);
}
},
iterateAllJsdocs: true,
meta: {
fixable: 'code',
type: 'suggestion',
},
});

0 comments on commit 7839625

Please sign in to comment.