Skip to content

Commit ed5c2a0

Browse files
committed
feat(lint): Identify explicit tags that don't match inference in lint stage
This will flag cases where people explicitly document things in JSDoc that don't reflect the code reality - in many cases, misnamed parameter names in documentation tags. Fixes #575
1 parent 0894cc4 commit ed5c2a0

File tree

14 files changed

+135
-96
lines changed

14 files changed

+135
-96
lines changed

declarations/comment.js

+1-15
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,7 @@ type CommentContextGitHub = {
5959
url: string
6060
};
6161

62-
type CommentTagBase = {
63-
title: string
64-
};
65-
66-
type CommentTag = CommentTagBase & {
67-
name?: string,
68-
title: string,
69-
description?: Object,
70-
default?: any,
71-
lineNumber?: number,
72-
type?: DoctrineType,
73-
properties?: Array<CommentTag>
74-
};
75-
76-
type CommentTagNamed = CommentTag & {
62+
type CommentTag = {
7763
name?: string,
7864
title: string,
7965
description?: Object,

lib/index.js

+9-10
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,16 @@ var fs = require('fs'),
2828

2929
/**
3030
* Build a pipeline of comment handlers.
31-
* @param {...Function|null} args - Pipeline elements. Each is a function that accepts
31+
* @param {Array<Function>} fns - Pipeline elements. Each is a function that accepts
3232
* a comment and can return a comment or undefined (to drop that comment).
3333
* @returns {Function} pipeline
3434
* @private
3535
*/
36-
function pipeline() {
37-
var elements = arguments;
36+
function pipeline(fns) {
3837
return comment => {
39-
for (var i = 0; comment && i < elements.length; i++) {
40-
if (elements[i]) {
41-
comment = elements[i](comment);
38+
for (var i = 0; comment && i < fns.length; i++) {
39+
if (fns[i]) {
40+
comment = fns[i](comment);
4241
}
4342
}
4443
return comment;
@@ -95,7 +94,7 @@ function buildInternal(inputsAndConfig) {
9594

9695
var parseFn = config.polyglot ? polyglot : parseJavaScript;
9796

98-
var buildPipeline = pipeline(
97+
var buildPipeline = pipeline([
9998
inferName,
10099
inferAccess(config.inferPrivate),
101100
inferAugments,
@@ -108,7 +107,7 @@ function buildInternal(inputsAndConfig) {
108107
inferType,
109108
config.github && github,
110109
garbageCollect
111-
);
110+
]);
112111

113112
let extractedComments = _.flatMap(inputs, function(sourceFile) {
114113
if (!sourceFile.source) {
@@ -130,7 +129,7 @@ function lintInternal(inputsAndConfig) {
130129

131130
let parseFn = config.polyglot ? polyglot : parseJavaScript;
132131

133-
let lintPipeline = pipeline(
132+
let lintPipeline = pipeline([
134133
lintComments,
135134
inferName,
136135
inferAccess(config.inferPrivate),
@@ -142,7 +141,7 @@ function lintInternal(inputsAndConfig) {
142141
inferMembership(),
143142
inferType,
144143
nest
145-
);
144+
]);
146145

147146
let extractedComments = _.flatMap(inputs, sourceFile => {
148147
if (!sourceFile.source) {

lib/infer/membership.js

-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,6 @@ function normalizeMemberof(comment /*: Comment*/) /*: Comment */ {
187187
* annotations within a file
188188
*
189189
* @private
190-
* @param {Object} comment parsed comment
191190
* @returns {Object} comment with membership inferred
192191
*/
193192
module.exports = function() {

lib/infer/params.js

+29-21
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ const _ = require('lodash');
77
const findTarget = require('./finders').findTarget;
88
const flowDoctrine = require('../flow_doctrine');
99
const util = require('util');
10-
const debuglog = util.debuglog('documentation');
1110

1211
/**
1312
* Infers param tags by reading function parameter names
@@ -45,17 +44,22 @@ function inferParams(comment /*: Comment */) {
4544

4645
function inferAndCombineParams(params, comment) {
4746
var inferredParams = params.map((param, i) => paramToDoc(param, '', i));
48-
var mergedParams = mergeTrees(inferredParams, comment.params);
47+
var mergedParamsAndErrors = mergeTrees(inferredParams, comment.params);
4948

5049
// Then merge the trees. This is the hard part.
5150
return _.assign(comment, {
52-
params: mergedParams
51+
params: mergedParamsAndErrors.mergedParams,
52+
errors: comment.errors.concat(mergedParamsAndErrors.errors)
5353
});
5454
}
5555

5656
// Utility methods ============================================================
5757
//
5858
const PATH_SPLIT_CAPTURING = /(\[])?(\.)/g;
59+
const PATH_SPLIT = /(?:\[])?\./g;
60+
function tagDepth(tag /*: CommentTag */) /*: number */ {
61+
return (tag.name || '').split(PATH_SPLIT).length;
62+
}
5963

6064
/**
6165
* Index tags by their `name` property into an ES6 map.
@@ -199,7 +203,7 @@ function paramToDoc(
199203
};
200204
default:
201205
// (a)
202-
var newParam /*: CommentTagNamed */ = {
206+
var newParam /*: CommentTag*/ = {
203207
title: 'param',
204208
name: prefix ? prefixedName : param.name,
205209
lineNumber: param.loc.start.line
@@ -261,25 +265,29 @@ function mergeTrees(inferred, explicit) {
261265
function mergeTopNodes(inferred, explicit) {
262266
const mapExplicit = mapTags(explicit);
263267
const inferredNames = new Set(inferred.map(tag => tag.name));
264-
const explicitTagsWithoutInference = explicit.filter(
265-
tag => !inferredNames.has(tag.name)
266-
);
268+
const explicitTagsWithoutInference = explicit.filter(tag => {
269+
return tagDepth(tag) === 1 && !inferredNames.has(tag.name);
270+
});
267271

268-
if (explicitTagsWithoutInference.length) {
269-
debuglog(
270-
`${explicitTagsWithoutInference.length} tags were specified but didn't match ` +
271-
`inferred information ${explicitTagsWithoutInference
272-
.map(t => t.name)
273-
.join(', ')}`
274-
);
275-
}
272+
var errors = explicitTagsWithoutInference.map(tag => {
273+
return {
274+
message: `An explicit parameter named ${tag.name || ''} was specified but didn't match ` +
275+
`inferred information ${Array.from(inferredNames).join(', ')}`,
276+
commentLineNumber: tag.lineNumber
277+
};
278+
});
276279

277-
return inferred
278-
.map(inferredTag => {
279-
const explicitTag = mapExplicit.get(inferredTag.name);
280-
return explicitTag ? combineTags(inferredTag, explicitTag) : inferredTag;
281-
})
282-
.concat(explicitTagsWithoutInference);
280+
return {
281+
errors,
282+
mergedParams: inferred
283+
.map(inferredTag => {
284+
const explicitTag = mapExplicit.get(inferredTag.name);
285+
return explicitTag
286+
? combineTags(inferredTag, explicitTag)
287+
: inferredTag;
288+
})
289+
.concat(explicitTagsWithoutInference)
290+
};
283291
}
284292

285293
// This method is used for _non-root_ properties only - we use mergeTopNodes

lib/input/shallow.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ var smartGlob = require('../smart_glob.js');
1616
* or without fs access.
1717
*
1818
* @param indexes entry points
19-
* @param options parsing options
19+
* @param config parsing options
2020
* @return promise with parsed files
2121
*/
2222
module.exports = function(

lib/nest.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const PATH_SPLIT = /(?:\[])?\./g;
77

88
function removeUnnamedTags(
99
tags /*: Array<CommentTag> */
10-
) /*: Array<CommentTagNamed> */ {
10+
) /*: Array<CommentTag> */ {
1111
return tags.filter(tag => typeof tag.name === 'string');
1212
}
1313

@@ -32,9 +32,7 @@ var tagDepth = tag => tag.name.split(PATH_SPLIT).length;
3232
* \-> [].baz
3333
*
3434
* @private
35-
* @param {Object} comment a comment with tags
36-
* @param {string} tagTitle the tag to nest
37-
* @param {string} target the tag to nest into
35+
* @param {Array<CommentTag>} tags a list of tags
3836
* @returns {Object} nested comment
3937
*/
4038
var nestTag = (

lib/output/html.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ var mergeConfig = require('../merge_config');
88
* Formats documentation as HTML.
99
*
1010
* @param comments parsed comments
11-
* @param {Object} args Options that can customize the output
12-
* @param {string} [args.theme='default_theme'] Name of a module used for an HTML theme.
11+
* @param {Object} config Options that can customize the output
12+
* @param {string} [config.theme='default_theme'] Name of a module used for an HTML theme.
1313
* @returns {Promise<Array<Object>>} Promise with results
1414
* @name formats.html
1515
* @public

lib/output/util/formatters.js

-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ module.exports = function(getHref /*: Function*/) {
7272
/**
7373
* Link text to this page or to a central resource.
7474
* @param {string} text inner text of the link
75-
* @param {string} description link text override
7675
* @returns {string} potentially linked HTML
7776
*/
7877
formatters.autolink = function(text /*: string*/) {

lib/parse.js

+11
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,17 @@ function parseJSDoc(
600600
result.description = parseMarkdown(result.description);
601601
}
602602

603+
// Reject parameter tags without a parameter name
604+
result.tags.filter(function(tag) {
605+
if (tag.title === 'param' && tag.name === undefined) {
606+
result.errors.push({
607+
message: 'A @param tag without a parameter name was rejected'
608+
});
609+
return false;
610+
}
611+
return true;
612+
});
613+
603614
result.tags.forEach(function(tag) {
604615
if (tag.errors) {
605616
for (var j = 0; j < tag.errors.length; j++) {

lib/parsers/polyglot.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ var getComments = require('get-comments'),
1010
* Documentation stream parser: this receives a module-dep item,
1111
* reads the file, parses the JavaScript, parses the JSDoc, and
1212
* emits parsed comments.
13-
* @param {Object} data a chunk of data provided by module-deps
13+
* @param sourceFile a chunk of data provided by module-deps
1414
* @return {Array<Object>} adds to memo
1515
*/
1616
function parsePolyglot(sourceFile /*: SourceFile*/) {

test/fixture/lint/lint.input.js

+6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* @param {Array<Number>} foo bar
44
* @param {Array<Number>} foo bar
55
* @param {Array|Number} foo bar
6+
* @param {boolean}
67
* @returns {object} bad object return type
78
* @type {Array<object>} bad object type
89
* @memberof notfound
@@ -13,3 +14,8 @@
1314
* @property {String} bad property
1415
* @private
1516
*/
17+
18+
/**
19+
* @param {number} c explicit but not found
20+
*/
21+
function add(a, b) {}

test/fixture/lint/lint.output

+11-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
2:1 warning type String found, string is standard
1+
2:1 warning An explicit parameter named foo was specified but didn't match inferred information a, b
22
3:1 warning type Number found, number is standard
3+
3:1 warning An explicit parameter named foo was specified but didn't match inferred information a, b
34
4:1 warning type Number found, number is standard
5+
4:1 warning An explicit parameter named foo was specified but didn't match inferred information a, b
46
5:1 warning type Number found, number is standard
5-
6:1 warning type object found, Object is standard
7+
5:1 warning An explicit parameter named foo was specified but didn't match inferred information a, b
8+
6:1 warning An explicit parameter named boolean was specified but didn't match inferred information a, b
69
7:1 warning type object found, Object is standard
7-
8:1 warning @memberof reference to notfound not found
8-
11:1 warning could not determine @name for hierarchy
9-
12:1 warning type String found, string is standard
10+
8:1 warning type object found, Object is standard
11+
9:1 warning @memberof reference to notfound not found
1012
13:1 warning type String found, string is standard
13+
13:1 warning An explicit parameter named baz was specified but didn't match inferred information a, b
14+
14:1 warning type String found, string is standard
15+
19:1 warning An explicit parameter named c was specified but didn't match inferred information a, b
1116

12-
11 warnings
17+
16 warnings

test/fixture/params.output.json

+18-1
Original file line numberDiff line numberDiff line change
@@ -1212,7 +1212,24 @@
12121212
}
12131213
},
12141214
"augments": [],
1215-
"errors": [],
1215+
"errors": [
1216+
{
1217+
"message": "An explicit parameter named address was specified but didn't match inferred information ",
1218+
"commentLineNumber": 5
1219+
},
1220+
{
1221+
"message": "An explicit parameter named groups was specified but didn't match inferred information ",
1222+
"commentLineNumber": 6
1223+
},
1224+
{
1225+
"message": "An explicit parameter named third was specified but didn't match inferred information ",
1226+
"commentLineNumber": 7
1227+
},
1228+
{
1229+
"message": "An explicit parameter named foo was specified but didn't match inferred information ",
1230+
"commentLineNumber": 8
1231+
}
1232+
],
12161233
"examples": [
12171234
{
12181235
"description": "var address = new Address6('2001::/32');"

0 commit comments

Comments
 (0)