diff --git a/lib/rules/prefer-const.js b/lib/rules/prefer-const.js index 1395e0a8a08..e55b57ccd10 100644 --- a/lib/rules/prefer-const.js +++ b/lib/rules/prefer-const.js @@ -126,6 +126,7 @@ function getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign) { if (isReadBeforeInit) { return variable.defs[0].name; } + return writer.identifier; } @@ -173,6 +174,7 @@ function groupByDestructuring(variables, ignoreReadBeforeAssign) { const variable = variables[i]; const references = variable.references; const identifier = getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign); + let prevId = null; for (let j = 0; j < references.length; ++j) { @@ -202,6 +204,128 @@ function groupByDestructuring(variables, ignoreReadBeforeAssign) { return identifierMap; } +/** + * Returns a list of nodes + * that have have the type Identifier. + * This will search for nested Identifiers. + * + * @param {ASTNode} node - node to search for children and nested identifiers. + * @returns {ASTNode[]} list Identifier nodes. + */ +function getIdentifiersInArrayDestructureGroup(node) { + const identifiers = []; + + if (node.elements) { + const numberOfElements = node.elements.length; + + for (let i = 0; i < numberOfElements; i++) { + if (!node.elements[i].elements && node.elements[i].type === "Identifier") { + identifiers.push(node.elements[i]); + } else { + const innerIdentifiers = getIdentifiersInArrayDestructureGroup(node.elements[i]); + + innerIdentifiers.forEach(identifierNode => identifiers.push(identifierNode)); + } + } + } + return identifiers; +} + +/** + * Returns a count of nodes + * This will count nested nodes in nodes elements attribute. + * + * @param {ASTNode} node - node to count elements. + * @returns {integer} count nodes. + */ +function countElementsInArrayDestructureGroup(node) { + let count = 0; + + if (node.elements) { + const numberOfElements = node.elements.length; + + for (let i = 0; i < numberOfElements; i++) { + if (!node.elements[i].elements) { + count += 1; + } else { + count += countElementsInArrayDestructureGroup(node.elements[i]); + } + } + } + return count; +} + +/** + * Returns a list of nodes + * that have have the value type Identifier. + * This will search for nested Identifiers. + * + * @param {ASTNode} node - node to search properties that have value Identifier. + * @returns {ASTNode[]} list Identifier nodes. + */ +function getIdentifiersInObjectDestructureGroup(node) { + const identifier = []; + const propertiesLength = node.properties.length; + + for (let i = 0; i < propertiesLength; i++) { + if (node.properties[i].value.type === "Identifier") { + identifier.push(node.properties[i].value); + } + } + return identifier; +} + +/** + * Returns a count of nodes + * This will count the node properties length + * + * @param {ASTNode} node - node to count properties. + * @returns {integer} count properties length. + */ +function countElementsInObjectDestructureGroup(node) { + return node.properties.length; +} + +/** + * Checks to see if there are less identifier nodes in a destructure group + * than the number of terms in the group. It might mean that there + * is a term in the group that cannot be converted to const. + * We want to make sure all terms can be reported + * If not, we should remove the terms that would be reported in "any" case + * + * @param {Map} identifierMap - Variables to group by destructuring. + * @param {boolean} checkingMixedDestructuring - boolean to check if any value in destructuring + * should use const + * @param {integer|null} destructureCount count of destructure terms. + * @param {ASTNode[]|null} destructureIdentifier list of Identifier nodes to check + * in IdentifierMap + * @returns {Map} Grouped identifier nodes. + */ +function verifyAllDestructuring(identifierMap, checkingMixedDestructuring, destructureCount, destructureIdentifier) { + if (checkingMixedDestructuring) { + return identifierMap; + } + if (destructureCount === null && destructureIdentifier === null) { + return identifierMap; + } + if (destructureIdentifier.length < destructureCount) { + for (const key of identifierMap.keys()) { + const destructureGroup = identifierMap.get(key); + const destructureElement = destructureGroup[0]; + + if (destructureIdentifier !== null) { + for (let i = 0; i < destructureIdentifier.length; i++) { + if (destructureIdentifier[i] === destructureElement) { + identifierMap.delete(key); + } + } + } + } + } + + return identifierMap; +} + /** * Finds the nearest parent of node with a given type. * @@ -252,6 +376,8 @@ module.exports = { const checkingMixedDestructuring = options.destructuring !== "all"; const ignoreReadBeforeAssign = options.ignoreReadBeforeAssign === true; const variables = []; + let destructureCount = null; + let destructureIdentifier = null; /** * Reports given identifier nodes if all of the nodes should be declared @@ -300,7 +426,18 @@ module.exports = { return { "Program:exit"() { - groupByDestructuring(variables, ignoreReadBeforeAssign).forEach(checkGroup); + const identifierMap = groupByDestructuring(variables, ignoreReadBeforeAssign); + + verifyAllDestructuring(identifierMap, checkingMixedDestructuring, destructureCount, destructureIdentifier).forEach(checkGroup); + }, + "ExpressionStatement[expression.left.type = /ArrayPattern|ObjectPattern/]"(node) { + if (node.expression.left.type === "ArrayPattern") { + destructureIdentifier = getIdentifiersInArrayDestructureGroup(node.expression.left); + destructureCount = countElementsInArrayDestructureGroup(node.expression.left); + } else { + destructureIdentifier = getIdentifiersInObjectDestructureGroup(node.expression.left); + destructureCount = countElementsInObjectDestructureGroup(node.expression.left); + } }, VariableDeclaration(node) { diff --git a/tests/lib/rules/prefer-const.js b/tests/lib/rules/prefer-const.js index f389dcd3026..aa1e856deab 100644 --- a/tests/lib/rules/prefer-const.js +++ b/tests/lib/rules/prefer-const.js @@ -56,6 +56,31 @@ ruleTester.run("prefer-const", rule, { "let a; function foo() { if (a) {} a = bar(); }", "let a; function foo() { a = a || bar(); baz(a); }", "let a; function foo() { bar(++a); }", + { + code: "let foo; ({ x: bar.baz, y: foo } = qux);", + options: [{ destructuring: "all" }] + }, + { + code: "let foo; ({ x: bar.baz, y: foo, z: bar.bro.q } = qux);", + options: [{ destructuring: "all" }] + }, + { + code: "let foo; [foo, [bar.baz]] = qux;", + options: [{ destructuring: "all" }] + }, + { + code: "let foo, bar; [foo, [bar, baz.qux]] = qux;", + output: null, + options: [{ destructuring: "all" }] + }, + { + code: "let predicate; [typeNode.returnType, predicate] = foo();", + options: [{ destructuring: "all" }] + }, + { + code: "let predicate; let rest; [typeNode.returnType, predicate, ...rest] = foo();", + options: [{ destructuring: "all" }] + }, [ "let id;", "function foo() {", @@ -106,6 +131,11 @@ ruleTester.run("prefer-const", rule, { } ], invalid: [ + { + code: "let foo; [foo, []] = qux;", + output: null, + errors: [{ message: "'foo' is never reassigned. Use 'const' instead.", type: "Identifier" }] + }, { code: "let x = 1; foo(x);", output: "const x = 1; foo(x);", @@ -268,6 +298,14 @@ ruleTester.run("prefer-const", rule, { { message: "'a' is never reassigned. Use 'const' instead.", type: "Identifier" } ] }, + { + code: "let [ foo, bar ] = baz();", + output: "const [ foo, bar ] = baz();", + errors: [ + { message: "'foo' is never reassigned. Use 'const' instead.", type: "Identifier" }, + { message: "'bar' is never reassigned. Use 'const' instead.", type: "Identifier" } + ] + }, { code: "let {a} = obj", output: "const {a} = obj", @@ -343,6 +381,36 @@ ruleTester.run("prefer-const", rule, { { message: "'foo' is never reassigned. Use 'const' instead.", type: "Identifier" }, { message: "'bar' is never reassigned. Use 'const' instead.", type: "Identifier" } ] + }, + { + code: "let predicate; [predicate, typeNode.returnType] = foo();", + output: null, + errors: [{ message: "'predicate' is never reassigned. Use 'const' instead.", type: "Identifier" }] + }, + { + code: "let predicate; [typeNode.returnType, predicate] = foo();", + output: null, + errors: [{ message: "'predicate' is never reassigned. Use 'const' instead.", type: "Identifier" }] + }, + { + code: "let predicate; let rest; [typeNode.returnType, predicate, ...rest] = foo();", + output: null, + errors: [ + { message: "'predicate' is never reassigned. Use 'const' instead.", type: "Identifier" }, + { message: "'rest' is never reassigned. Use 'const' instead.", type: "Identifier" } + ] + }, + { + code: "let foo; [foo, [bar.baz]] = qux;", + output: null, + options: [{ destructuring: "any" }], + errors: [{ message: "'foo' is never reassigned. Use 'const' instead.", type: "Identifier" }] + }, + { + code: "let foo; ({ x: bar.baz, y: foo } = qux);", + output: null, + options: [{ destructuring: "any" }], + errors: [{ message: "'foo' is never reassigned. Use 'const' instead.", type: "Identifier" }] } ] });