formio/node_modules/eslint/lib/rules/valid-jsdoc.js   F
last analyzed

Complexity

Total Complexity 83
Complexity/F 6.38

Size

Lines of Code 420
Function Count 13

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 0
wmc 83
nc 849346560
mnd 5
bc 44
fnc 13
dl 0
loc 420
rs 1.5789
bpm 3.3846
cpm 6.3846
noi 3
c 0
b 0
f 0

1 Function

Rating   Name   Duplication   Size   Complexity  
B module.exports.create 0 361 1

How to fix   Complexity   

Complexity

Complex classes like formio/node_modules/eslint/lib/rules/valid-jsdoc.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/**
2
 * @fileoverview Validates JSDoc comments are syntactically correct
3
 * @author Nicholas C. Zakas
4
 */
5
"use strict";
6
7
//------------------------------------------------------------------------------
8
// Requirements
9
//------------------------------------------------------------------------------
10
11
const doctrine = require("doctrine");
12
13
//------------------------------------------------------------------------------
14
// Rule Definition
15
//------------------------------------------------------------------------------
16
17
module.exports = {
18
    meta: {
19
        docs: {
20
            description: "enforce valid JSDoc comments",
21
            category: "Possible Errors",
22
            recommended: false,
23
            url: "https://eslint.org/docs/rules/valid-jsdoc"
24
        },
25
26
        schema: [
27
            {
28
                type: "object",
29
                properties: {
30
                    prefer: {
31
                        type: "object",
32
                        additionalProperties: {
33
                            type: "string"
34
                        }
35
                    },
36
                    preferType: {
37
                        type: "object",
38
                        additionalProperties: {
39
                            type: "string"
40
                        }
41
                    },
42
                    requireReturn: {
43
                        type: "boolean"
44
                    },
45
                    requireParamDescription: {
46
                        type: "boolean"
47
                    },
48
                    requireReturnDescription: {
49
                        type: "boolean"
50
                    },
51
                    matchDescription: {
52
                        type: "string"
53
                    },
54
                    requireReturnType: {
55
                        type: "boolean"
56
                    }
57
                },
58
                additionalProperties: false
59
            }
60
        ]
61
    },
62
63
    create(context) {
64
65
        const options = context.options[0] || {},
66
            prefer = options.prefer || {},
67
            sourceCode = context.getSourceCode(),
68
69
            // these both default to true, so you have to explicitly make them false
70
            requireReturn = options.requireReturn !== false,
71
            requireParamDescription = options.requireParamDescription !== false,
72
            requireReturnDescription = options.requireReturnDescription !== false,
73
            requireReturnType = options.requireReturnType !== false,
74
            preferType = options.preferType || {},
75
            checkPreferType = Object.keys(preferType).length !== 0;
76
77
        //--------------------------------------------------------------------------
78
        // Helpers
79
        //--------------------------------------------------------------------------
80
81
        // Using a stack to store if a function returns or not (handling nested functions)
82
        const fns = [];
83
84
        /**
85
         * Check if node type is a Class
86
         * @param {ASTNode} node node to check.
87
         * @returns {boolean} True is its a class
88
         * @private
89
         */
90
        function isTypeClass(node) {
91
            return node.type === "ClassExpression" || node.type === "ClassDeclaration";
92
        }
93
94
        /**
95
         * When parsing a new function, store it in our function stack.
96
         * @param {ASTNode} node A function node to check.
97
         * @returns {void}
98
         * @private
99
         */
100
        function startFunction(node) {
101
            fns.push({
102
                returnPresent: (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") ||
103
                    isTypeClass(node)
104
            });
105
        }
106
107
        /**
108
         * Indicate that return has been found in the current function.
109
         * @param {ASTNode} node The return node.
110
         * @returns {void}
111
         * @private
112
         */
113
        function addReturn(node) {
114
            const functionState = fns[fns.length - 1];
115
116
            if (functionState && node.argument !== null) {
117
                functionState.returnPresent = true;
118
            }
119
        }
120
121
        /**
122
         * Check if return tag type is void or undefined
123
         * @param {Object} tag JSDoc tag
124
         * @returns {boolean} True if its of type void or undefined
125
         * @private
126
         */
127
        function isValidReturnType(tag) {
128
            return tag.type === null || tag.type.name === "void" || tag.type.type === "UndefinedLiteral";
129
        }
130
131
        /**
132
         * Check if type should be validated based on some exceptions
133
         * @param {Object} type JSDoc tag
134
         * @returns {boolean} True if it can be validated
135
         * @private
136
         */
137
        function canTypeBeValidated(type) {
138
            return type !== "UndefinedLiteral" && // {undefined} as there is no name property available.
139
                   type !== "NullLiteral" && // {null}
140
                   type !== "NullableLiteral" && // {?}
141
                   type !== "FunctionType" && // {function(a)}
142
                   type !== "AllLiteral"; // {*}
143
        }
144
145
        /**
146
         * Extract the current and expected type based on the input type object
147
         * @param {Object} type JSDoc tag
148
         * @returns {Object} current and expected type object
149
         * @private
150
         */
151
        function getCurrentExpectedTypes(type) {
152
            let currentType;
153
154
            if (type.name) {
155
                currentType = type.name;
156
            } else if (type.expression) {
157
                currentType = type.expression.name;
158
            }
159
160
            const expectedType = currentType && preferType[currentType];
161
162
            return {
163
                currentType,
0 ignored issues
show
Bug introduced by
The variable currentType does not seem to be initialized in case type.expression on line 156 is false. Are you sure this can never be the case?
Loading history...
164
                expectedType
165
            };
166
        }
167
168
        /**
169
         * Validate type for a given JSDoc node
170
         * @param {Object} jsdocNode JSDoc node
171
         * @param {Object} type JSDoc tag
172
         * @returns {void}
173
         * @private
174
         */
175
        function validateType(jsdocNode, type) {
176
            if (!type || !canTypeBeValidated(type.type)) {
177
                return;
178
            }
179
180
            const typesToCheck = [];
181
            let elements = [];
182
183
            switch (type.type) {
184
                case "TypeApplication": // {Array.<String>}
185
                    elements = type.applications[0].type === "UnionType" ? type.applications[0].elements : type.applications;
186
                    typesToCheck.push(getCurrentExpectedTypes(type));
187
                    break;
188
                case "RecordType": // {{20:String}}
189
                    elements = type.fields;
190
                    break;
191
                case "UnionType": // {String|number|Test}
192
                case "ArrayType": // {[String, number, Test]}
193
                    elements = type.elements;
194
                    break;
195
                case "FieldType": // Array.<{count: number, votes: number}>
196
                    if (type.value) {
197
                        typesToCheck.push(getCurrentExpectedTypes(type.value));
198
                    }
199
                    break;
200
                default:
201
                    typesToCheck.push(getCurrentExpectedTypes(type));
202
            }
203
204
            elements.forEach(validateType.bind(null, jsdocNode));
0 ignored issues
show
Unused Code introduced by
The call to bind does not seem necessary since the function validateType declared on line 175 does not use this.
Loading history...
205
206
            typesToCheck.forEach(typeToCheck => {
207
                if (typeToCheck.expectedType &&
208
                    typeToCheck.expectedType !== typeToCheck.currentType) {
209
                    context.report({
210
                        node: jsdocNode,
211
                        message: "Use '{{expectedType}}' instead of '{{currentType}}'.",
212
                        data: {
213
                            currentType: typeToCheck.currentType,
214
                            expectedType: typeToCheck.expectedType
215
                        }
216
                    });
217
                }
218
            });
219
        }
220
221
        /**
222
         * Validate the JSDoc node and output warnings if anything is wrong.
223
         * @param {ASTNode} node The AST node to check.
224
         * @returns {void}
225
         * @private
226
         */
227
        function checkJSDoc(node) {
228
            const jsdocNode = sourceCode.getJSDocComment(node),
229
                functionData = fns.pop(),
230
                params = Object.create(null),
231
                paramsTags = [];
232
            let hasReturns = false,
233
                returnsTag,
234
                hasConstructor = false,
235
                isInterface = false,
236
                isOverride = false,
237
                isAbstract = false;
238
239
            // make sure only to validate JSDoc comments
240
            if (jsdocNode) {
241
                let jsdoc;
242
243
                try {
244
                    jsdoc = doctrine.parse(jsdocNode.value, {
245
                        strict: true,
246
                        unwrap: true,
247
                        sloppy: true
248
                    });
249
                } catch (ex) {
250
251
                    if (/braces/i.test(ex.message)) {
252
                        context.report({ node: jsdocNode, message: "JSDoc type missing brace." });
253
                    } else {
254
                        context.report({ node: jsdocNode, message: "JSDoc syntax error." });
255
                    }
256
257
                    return;
258
                }
259
260
                jsdoc.tags.forEach(tag => {
261
262
                    switch (tag.title.toLowerCase()) {
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
263
264
                        case "param":
265
                        case "arg":
266
                        case "argument":
267
                            paramsTags.push(tag);
268
                            break;
269
270
                        case "return":
271
                        case "returns":
272
                            hasReturns = true;
273
                            returnsTag = tag;
274
                            break;
275
276
                        case "constructor":
277
                        case "class":
278
                            hasConstructor = true;
279
                            break;
280
281
                        case "override":
282
                        case "inheritdoc":
283
                            isOverride = true;
284
                            break;
285
286
                        case "abstract":
287
                        case "virtual":
288
                            isAbstract = true;
289
                            break;
290
291
                        case "interface":
292
                            isInterface = true;
293
                            break;
294
295
                        // no default
296
                    }
297
298
                    // check tag preferences
299
                    if (prefer.hasOwnProperty(tag.title) && tag.title !== prefer[tag.title]) {
300
                        context.report({ node: jsdocNode, message: "Use @{{name}} instead.", data: { name: prefer[tag.title] } });
301
                    }
302
303
                    // validate the types
304
                    if (checkPreferType && tag.type) {
305
                        validateType(jsdocNode, tag.type);
306
                    }
307
                });
308
309
                paramsTags.forEach(param => {
310
                    if (!param.type) {
311
                        context.report({ node: jsdocNode, message: "Missing JSDoc parameter type for '{{name}}'.", data: { name: param.name } });
312
                    }
313
                    if (!param.description && requireParamDescription) {
314
                        context.report({ node: jsdocNode, message: "Missing JSDoc parameter description for '{{name}}'.", data: { name: param.name } });
315
                    }
316
                    if (params[param.name]) {
317
                        context.report({ node: jsdocNode, message: "Duplicate JSDoc parameter '{{name}}'.", data: { name: param.name } });
318
                    } else if (param.name.indexOf(".") === -1) {
319
                        params[param.name] = 1;
320
                    }
321
                });
322
323
                if (hasReturns) {
324
                    if (!requireReturn && !functionData.returnPresent && (returnsTag.type === null || !isValidReturnType(returnsTag)) && !isAbstract) {
325
                        context.report({
326
                            node: jsdocNode,
327
                            message: "Unexpected @{{title}} tag; function has no return statement.",
328
                            data: {
329
                                title: returnsTag.title
330
                            }
331
                        });
332
                    } else {
333
                        if (requireReturnType && !returnsTag.type) {
334
                            context.report({ node: jsdocNode, message: "Missing JSDoc return type." });
335
                        }
336
337
                        if (!isValidReturnType(returnsTag) && !returnsTag.description && requireReturnDescription) {
338
                            context.report({ node: jsdocNode, message: "Missing JSDoc return description." });
339
                        }
340
                    }
341
                }
342
343
                // check for functions missing @returns
344
                if (!isOverride && !hasReturns && !hasConstructor && !isInterface &&
345
                    node.parent.kind !== "get" && node.parent.kind !== "constructor" &&
346
                    node.parent.kind !== "set" && !isTypeClass(node)) {
347
                    if (requireReturn || functionData.returnPresent) {
348
                        context.report({
349
                            node: jsdocNode,
350
                            message: "Missing JSDoc @{{returns}} for function.",
351
                            data: {
352
                                returns: prefer.returns || "returns"
353
                            }
354
                        });
355
                    }
356
                }
357
358
                // check the parameters
359
                const jsdocParams = Object.keys(params);
360
361
                if (node.params) {
362
                    node.params.forEach((param, i) => {
363
                        if (param.type === "AssignmentPattern") {
364
                            param = param.left;
365
                        }
366
367
                        const name = param.name;
368
369
                        // TODO(nzakas): Figure out logical things to do with destructured, default, rest params
370
                        if (param.type === "Identifier") {
371
                            if (jsdocParams[i] && (name !== jsdocParams[i])) {
372
                                context.report({
373
                                    node: jsdocNode,
374
                                    message: "Expected JSDoc for '{{name}}' but found '{{jsdocName}}'.",
375
                                    data: {
376
                                        name,
377
                                        jsdocName: jsdocParams[i]
378
                                    }
379
                                });
380
                            } else if (!params[name] && !isOverride) {
381
                                context.report({
382
                                    node: jsdocNode,
383
                                    message: "Missing JSDoc for parameter '{{name}}'.",
384
                                    data: {
385
                                        name
386
                                    }
387
                                });
388
                            }
389
                        }
390
                    });
391
                }
392
393
                if (options.matchDescription) {
394
                    const regex = new RegExp(options.matchDescription);
395
396
                    if (!regex.test(jsdoc.description)) {
397
                        context.report({ node: jsdocNode, message: "JSDoc description does not satisfy the regex pattern." });
398
                    }
399
                }
400
401
            }
402
403
        }
404
405
        //--------------------------------------------------------------------------
406
        // Public
407
        //--------------------------------------------------------------------------
408
409
        return {
410
            ArrowFunctionExpression: startFunction,
411
            FunctionExpression: startFunction,
412
            FunctionDeclaration: startFunction,
413
            ClassExpression: startFunction,
414
            ClassDeclaration: startFunction,
415
            "ArrowFunctionExpression:exit": checkJSDoc,
416
            "FunctionExpression:exit": checkJSDoc,
417
            "FunctionDeclaration:exit": checkJSDoc,
418
            "ClassExpression:exit": checkJSDoc,
419
            "ClassDeclaration:exit": checkJSDoc,
420
            ReturnStatement: addReturn
421
        };
422
423
    }
424
};
425