Skip to content

Commit

Permalink
Merge branch 'master' into upgrade-typescript-eslint-2
Browse files Browse the repository at this point in the history
  • Loading branch information
vilchik-elena committed Oct 28, 2019
2 parents 94cdf20 + 4bac9a9 commit 513e564
Show file tree
Hide file tree
Showing 15 changed files with 804 additions and 33 deletions.
2 changes: 2 additions & 0 deletions eslint-bridge/src/rules/main.ts
Expand Up @@ -40,6 +40,7 @@ import { rule as noRedundantJump } from "./no-redundant-jump";
import { rule as cyclomaticComplexity } from "./cyclomatic-complexity";
import { rule as noAlphabeticalSort } from "./no-alphabetical-sort";
import { rule as noTab } from "./no-tab";
import { rule as noEmptyCollection } from "./no-empty-collection";
import { rule as nonExistentOperator } from "./non-existent-operator";
import { rule as noInMisuse } from "./no-in-misuse";
import { rule as noInvariantReturns } from "./no-invariant-returns";
Expand Down Expand Up @@ -93,6 +94,7 @@ ruleModules["no-redundant-jump"] = noRedundantJump;
ruleModules["cyclomatic-complexity"] = cyclomaticComplexity;
ruleModules["no-alphabetical-sort"] = noAlphabeticalSort;
ruleModules["no-tab"] = noTab;
ruleModules["no-empty-collection"] = noEmptyCollection;
ruleModules["non-existent-operator"] = nonExistentOperator;
ruleModules["no-in-misuse"] = noInMisuse;
ruleModules["no-invariant-returns"] = noInvariantReturns;
Expand Down
201 changes: 201 additions & 0 deletions eslint-bridge/src/rules/no-empty-collection.ts
@@ -0,0 +1,201 @@
/*
* SonarQube JavaScript Plugin
* Copyright (C) 2011-2019 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// https://jira.sonarsource.com/browse/RSPEC-4158

import { Rule, Scope } from "eslint";
import { isIdentifier, findFirstMatchingAncestor, isReferenceTo, ancestorsChain } from "./utils";
import { TSESTree } from "@typescript-eslint/experimental-utils";
import * as estree from "estree";
import { collectionConstructor } from "../utils/collections";

// Methods that mutate the collection but can't add elements
const nonAdditiveMutatorMethods = [
// array methods
"copyWithin",
"pop",
"reverse",
"shift",
"sort",
// map, set methods
"clear",
"delete",
];
const accessorMethods = [
// array methods
"concat",
"flat",
"flatMap",
"includes",
"indexOf",
"join",
"lastIndexOf",
"slice",
"toSource",
"toString",
"toLocaleString",
// map, set methods
"get",
"has",
];
const iterationMethods = [
"entries",
"every",
"filter",
"find",
"findIndex",
"forEach",
"keys",
"map",
"reduce",
"reduceRight",
"some",
"values",
];

const strictlyReadingMethods = new Set([
...nonAdditiveMutatorMethods,
...accessorMethods,
...iterationMethods,
]);

export const rule: Rule.RuleModule = {
create(context: Rule.RuleContext) {
return {
"Program:exit": function() {
reportEmptyCollectionsUsage(context.getScope(), context);
},
};
},
};

export function reportEmptyCollectionsUsage(scope: Scope.Scope, context: Rule.RuleContext) {
if (scope.type !== "global") {
scope.variables.forEach(v => {
reportEmptyCollectionUsage(v, context);
});
}

scope.childScopes.forEach(childScope => {
reportEmptyCollectionsUsage(childScope, context);
});
}

function reportEmptyCollectionUsage(variable: Scope.Variable, context: Rule.RuleContext) {
if (variable.references.length <= 1) {
return;
}

const usedReferences = [];
let isEmptyCollection = false;

for (const ref of variable.references) {
if (ref.isWriteOnly()) {
isEmptyCollection = isReferenceAssigningEmptyCollection(ref);
} else {
if (isReadCollectionPattern(ref)) {
if (isEmptyCollection) {
usedReferences.push(ref);
}
} else {
// One references is a write
return;
}
}
}

usedReferences.forEach(ref => {
context.report({
message: `Review this usage of "${ref.identifier.name}" as it can only be empty here.`,
node: ref.identifier,
});
});
}

function isReferenceAssigningEmptyCollection(ref: Scope.Reference) {
const declOrExprStmt = findFirstMatchingAncestor(
ref.identifier as TSESTree.Node,
n => n.type === "VariableDeclarator" || n.type === "ExpressionStatement",
) as estree.Node;
if (declOrExprStmt) {
if (declOrExprStmt.type === "VariableDeclarator" && declOrExprStmt.init) {
return isEmptyCollectionType(declOrExprStmt.init);
}

if (declOrExprStmt.type === "ExpressionStatement") {
const expression = declOrExprStmt.expression;
return (
expression.type === "AssignmentExpression" &&
isReferenceTo(ref, expression.left) &&
isEmptyCollectionType(expression.right)
);
}
}
return false;
}

function isEmptyCollectionType(node: estree.Node) {
if (node && node.type === "ArrayExpression") {
return node.elements.length === 0;
} else if (node && (node.type === "CallExpression" || node.type === "NewExpression")) {
return isIdentifier(node.callee, ...collectionConstructor) && node.arguments.length === 0;
}
return false;
}

function isReadCollectionPattern(ref: Scope.Reference) {
return isStrictlyReadingMethodCall(ref) || isForIterationPattern(ref) || isElementRead(ref);
}

function isStrictlyReadingMethodCall(usage: Scope.Reference) {
const parent = (usage.identifier as TSESTree.Node).parent;
if (parent && parent.type === "MemberExpression") {
const memberExpressionParent = parent.parent;
if (memberExpressionParent && memberExpressionParent.type === "CallExpression") {
return isIdentifier(parent.property as estree.Node, ...strictlyReadingMethods);
}
}
return false;
}

function isForIterationPattern(ref: Scope.Reference) {
const forInOrOfStatement = findFirstMatchingAncestor(
ref.identifier as TSESTree.Node,
n => n.type === "ForOfStatement" || n.type === "ForInStatement",
) as TSESTree.ForOfStatement | TSESTree.ForInStatement;

return forInOrOfStatement && forInOrOfStatement.right === ref.identifier;
}

function isElementRead(ref: Scope.Reference) {
const parent = (ref.identifier as TSESTree.Node).parent;

return parent && parent.type === "MemberExpression" && parent.computed && !isElementWrite(parent);
}

function isElementWrite(memberExpression: TSESTree.MemberExpression) {
const ancestors = ancestorsChain(memberExpression, new Set());
const assignment = ancestors.find(
n => n.type === "AssignmentExpression",
) as TSESTree.AssignmentExpression;
if (assignment && assignment.operator === "=") {
return [memberExpression, ...ancestors].includes(assignment.left);
}
return false;
}
19 changes: 1 addition & 18 deletions eslint-bridge/src/rules/no-unused-collection.ts
Expand Up @@ -23,27 +23,10 @@ import { Rule, Scope } from "eslint";
import * as estree from "estree";
import { TSESTree } from "@typescript-eslint/experimental-utils";
import { findFirstMatchingAncestor, isElementWrite, isIdentifier, isReferenceTo } from "./utils";
import { collectionConstructor, writingMethods } from "../utils/collections";

const message = "Either use this collection's contents or remove the collection.";

const writingMethods = [
"copyWithin",
"fill",
"pop",
"push",
"reverse",
"shift",
"sort",
"splice",
"unshift",
"clear",
"delete",
"set",
"add",
];

const collectionConstructor = ["Array", "Set", "Map", "WeakSet", "WeakMap"];

export const rule: Rule.RuleModule = {
create(context: Rule.RuleContext) {
return {
Expand Down
28 changes: 14 additions & 14 deletions eslint-bridge/src/rules/utils.ts
Expand Up @@ -202,36 +202,36 @@ function toSecondaryLocation(
};
}

export function findFirstMatchingAncestor(
export function findFirstMatchingLocalAncestor(
node: TSESTree.Node,
predicate: (node: TSESTree.Node) => boolean,
) {
return findFirstMatchingAncestorWithBoundaries(node, predicate, new Set());
return localAncestorsChain(node).find(predicate);
}

export function findFirstMatchingLocalAncestor(
export function findFirstMatchingAncestor(
node: TSESTree.Node,
predicate: (node: TSESTree.Node) => boolean,
) {
return findFirstMatchingAncestorWithBoundaries(node, predicate, functionLike);
return ancestorsChain(node, new Set()).find(predicate);
}

export function findFirstMatchingAncestorWithBoundaries(
node: TSESTree.Node,
predicate: (node: TSESTree.Node) => boolean,
boundaryTypes: Set<string>,
) {
export function localAncestorsChain(node: TSESTree.Node) {
return ancestorsChain(node, functionLike);
}

export function ancestorsChain(node: TSESTree.Node, boundaryTypes: Set<string>) {
const chain: TSESTree.Node[] = [];

let currentNode = node.parent;
while (currentNode) {
if (predicate(currentNode)) {
return currentNode;
}
chain.push(currentNode);
if (boundaryTypes.has(currentNode.type)) {
return undefined;
break;
}
currentNode = currentNode.parent;
}
return undefined;
return chain;
}

/**
Expand Down
39 changes: 39 additions & 0 deletions eslint-bridge/src/utils/collections.ts
@@ -0,0 +1,39 @@
/*
* SonarQube JavaScript Plugin
* Copyright (C) 2011-2019 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

export const collectionConstructor = ["Array", "Map", "Set", "WeakSet", "WeakMap"];

export const writingMethods = [
// array methods
"copyWithin",
"fill",
"pop",
"push",
"reverse",
"set",
"shift",
"sort",
"splice",
"unshift",
// map, set methods
"add",
"clear",
"delete",
];

0 comments on commit 513e564

Please sign in to comment.