/** * @fileoverview Rule to control usage of strict mode directives. * @author Brandon Mills */ "use strict"; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const astUtils = require("./utils/ast-utils"); //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ /** * Gets all of the Use Strict Directives in the Directive Prologue of a group of * statements. * @param {ASTNode[]} statements Statements in the program or function body. * @returns {ASTNode[]} All of the Use Strict Directives. */ function getUseStrictDirectives(statements) { const directives = []; for (let i = 0; i < statements.length; i++) { const statement = statements[i]; if ( statement.type === "ExpressionStatement" && statement.expression.type === "Literal" && statement.expression.value === "use strict" ) { directives[i] = statement; } else { break; } } return directives; } /** * Checks whether a given parameter is a simple parameter. * * @param {ASTNode} node - A pattern node to check. * @returns {boolean} `true` if the node is an Identifier node. */ function isSimpleParameter(node) { return node.type === "Identifier"; } /** * Checks whether a given parameter list is a simple parameter list. * * @param {ASTNode[]} params - A parameter list to check. * @returns {boolean} `true` if the every parameter is an Identifier node. */ function isSimpleParameterList(params) { return params.every(isSimpleParameter); } //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ module.exports = { meta: { type: "suggestion", docs: { description: "require or disallow strict mode directives", category: "Strict Mode", recommended: false, url: "https://eslint.org/docs/rules/strict" }, schema: [ { enum: ["never", "global", "function", "safe"] } ], fixable: "code", messages: { function: "Use the function form of 'use strict'.", global: "Use the global form of 'use strict'.", multiple: "Multiple 'use strict' directives.", never: "Strict mode is not permitted.", unnecessary: "Unnecessary 'use strict' directive.", module: "'use strict' is unnecessary inside of modules.", implied: "'use strict' is unnecessary when implied strict mode is enabled.", unnecessaryInClasses: "'use strict' is unnecessary inside of classes.", nonSimpleParameterList: "'use strict' directive inside a function with non-simple parameter list throws a syntax error since ES2016.", wrap: "Wrap {{name}} in a function with 'use strict' directive." } }, create(context) { const ecmaFeatures = context.parserOptions.ecmaFeatures || {}, scopes = [], classScopes = []; let mode = context.options[0] || "safe"; if (ecmaFeatures.impliedStrict) { mode = "implied"; } else if (mode === "safe") { mode = ecmaFeatures.globalReturn ? "global" : "function"; } /** * Determines whether a reported error should be fixed, depending on the error type. * @param {string} errorType The type of error * @returns {boolean} `true` if the reported error should be fixed */ function shouldFix(errorType) { return errorType === "multiple" || errorType === "unnecessary" || errorType === "module" || errorType === "implied" || errorType === "unnecessaryInClasses"; } /** * Gets a fixer function to remove a given 'use strict' directive. * @param {ASTNode} node The directive that should be removed * @returns {Function} A fixer function */ function getFixFunction(node) { return fixer => fixer.remove(node); } /** * Report a slice of an array of nodes with a given message. * @param {ASTNode[]} nodes Nodes. * @param {string} start Index to start from. * @param {string} end Index to end before. * @param {string} messageId Message to display. * @param {boolean} fix `true` if the directive should be fixed (i.e. removed) * @returns {void} */ function reportSlice(nodes, start, end, messageId, fix) { nodes.slice(start, end).forEach(node => { context.report({ node, messageId, fix: fix ? getFixFunction(node) : null }); }); } /** * Report all nodes in an array with a given message. * @param {ASTNode[]} nodes Nodes. * @param {string} messageId Message id to display. * @param {boolean} fix `true` if the directive should be fixed (i.e. removed) * @returns {void} */ function reportAll(nodes, messageId, fix) { reportSlice(nodes, 0, nodes.length, messageId, fix); } /** * Report all nodes in an array, except the first, with a given message. * @param {ASTNode[]} nodes Nodes. * @param {string} messageId Message id to display. * @param {boolean} fix `true` if the directive should be fixed (i.e. removed) * @returns {void} */ function reportAllExceptFirst(nodes, messageId, fix) { reportSlice(nodes, 1, nodes.length, messageId, fix); } /** * Entering a function in 'function' mode pushes a new nested scope onto the * stack. The new scope is true if the nested function is strict mode code. * @param {ASTNode} node The function declaration or expression. * @param {ASTNode[]} useStrictDirectives The Use Strict Directives of the node. * @returns {void} */ function enterFunctionInFunctionMode(node, useStrictDirectives) { const isInClass = classScopes.length > 0, isParentGlobal = scopes.length === 0 && classScopes.length === 0, isParentStrict = scopes.length > 0 && scopes[scopes.length - 1], isStrict = useStrictDirectives.length > 0; if (isStrict) { if (!isSimpleParameterList(node.params)) { context.report({ node: useStrictDirectives[0], messageId: "nonSimpleParameterList" }); } else if (isParentStrict) { context.report({ node: useStrictDirectives[0], messageId: "unnecessary", fix: getFixFunction(useStrictDirectives[0]) }); } else if (isInClass) { context.report({ node: useStrictDirectives[0], messageId: "unnecessaryInClasses", fix: getFixFunction(useStrictDirectives[0]) }); } reportAllExceptFirst(useStrictDirectives, "multiple", true); } else if (isParentGlobal) { if (isSimpleParameterList(node.params)) { context.report({ node, messageId: "function" }); } else { context.report({ node, messageId: "wrap", data: { name: astUtils.getFunctionNameWithKind(node) } }); } } scopes.push(isParentStrict || isStrict); } /** * Exiting a function in 'function' mode pops its scope off the stack. * @returns {void} */ function exitFunctionInFunctionMode() { scopes.pop(); } /** * Enter a function and either: * - Push a new nested scope onto the stack (in 'function' mode). * - Report all the Use Strict Directives (in the other modes). * @param {ASTNode} node The function declaration or expression. * @returns {void} */ function enterFunction(node) { const isBlock = node.body.type === "BlockStatement", useStrictDirectives = isBlock ? getUseStrictDirectives(node.body.body) : []; if (mode === "function") { enterFunctionInFunctionMode(node, useStrictDirectives); } else if (useStrictDirectives.length > 0) { if (isSimpleParameterList(node.params)) { reportAll(useStrictDirectives, mode, shouldFix(mode)); } else { context.report({ node: useStrictDirectives[0], messageId: "nonSimpleParameterList" }); reportAllExceptFirst(useStrictDirectives, "multiple", true); } } } const rule = { Program(node) { const useStrictDirectives = getUseStrictDirectives(node.body); if (node.sourceType === "module") { mode = "module"; } if (mode === "global") { if (node.body.length > 0 && useStrictDirectives.length === 0) { context.report({ node, messageId: "global" }); } reportAllExceptFirst(useStrictDirectives, "multiple", true); } else { reportAll(useStrictDirectives, mode, shouldFix(mode)); } }, FunctionDeclaration: enterFunction, FunctionExpression: enterFunction, ArrowFunctionExpression: enterFunction }; if (mode === "function") { Object.assign(rule, { // Inside of class bodies are always strict mode. ClassBody() { classScopes.push(true); }, "ClassBody:exit"() { classScopes.pop(); }, "FunctionDeclaration:exit": exitFunctionInFunctionMode, "FunctionExpression:exit": exitFunctionInFunctionMode, "ArrowFunctionExpression:exit": exitFunctionInFunctionMode }); } return rule; } };