diff --git a/src/helpers.ts b/src/helpers.ts index a5bebb7f..2f0a087e 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -11,6 +11,7 @@ import { BrowsersListOpts, } from "./types"; import { TargetNameMappings } from "./constants"; +import { RulesLookup, lookupKey } from "./rules-lookup"; /* 3) Figures out which browsers user is targeting @@ -51,13 +52,13 @@ function checkNotInsideIfStatementAndReport( export function lintCallExpression( context: Context, handleFailingRule: HandleFailingRule, - rules: AstMetadataApiWithTargetsResolver[], + rulesByObject: RulesLookup, sourceCode: SourceCode, node: ESLintNode ) { if (!node.callee) return; const calleeName = node.callee.name; - const failingRule = rules.find((rule) => rule.object === calleeName); + const failingRule = rulesByObject.get(calleeName); if (failingRule) checkNotInsideIfStatementAndReport( context, @@ -71,13 +72,13 @@ export function lintCallExpression( export function lintNewExpression( context: Context, handleFailingRule: HandleFailingRule, - rules: Array, + rulesByObject: RulesLookup, sourceCode: SourceCode, node: ESLintNode ) { if (!node.callee) return; const calleeName = node.callee.name; - const failingRule = rules.find((rule) => rule.object === calleeName); + const failingRule = rulesByObject.get(calleeName); if (failingRule) checkNotInsideIfStatementAndReport( context, @@ -91,14 +92,12 @@ export function lintNewExpression( export function lintExpressionStatement( context: Context, handleFailingRule: HandleFailingRule, - rules: AstMetadataApiWithTargetsResolver[], + rulesByObject: RulesLookup, sourceCode: SourceCode, node: ESLintNode ) { if (!node?.expression?.name) return; - const failingRule = rules.find( - (rule) => rule.object === node?.expression?.name - ); + const failingRule = rulesByObject.get(node?.expression?.name); if (failingRule) checkNotInsideIfStatementAndReport( context, @@ -135,7 +134,8 @@ function protoChainFromMemberExpression(node: ESLintNode): string[] { export function lintMemberExpression( context: Context, handleFailingRule: HandleFailingRule, - rules: Array, + rulesByProtoChainId: RulesLookup, + rulesByObjectProperty: RulesLookup, sourceCode: SourceCode, node: ESLintNode ) { @@ -152,9 +152,7 @@ export function lintMemberExpression( ? rawProtoChain.slice(1) : rawProtoChain; const protoChainId = protoChain.join("."); - const failingRule = rules.find( - (rule) => rule.protoChainId === protoChainId - ); + const failingRule = rulesByProtoChainId.get(protoChainId); if (failingRule) { checkNotInsideIfStatementAndReport( context, @@ -167,11 +165,9 @@ export function lintMemberExpression( } else { const objectName = node.object.name; const propertyName = node.property.name; - const failingRule = rules.find( - (rule) => - rule.object === objectName && - (rule.property == null || rule.property === propertyName) - ); + const failingRule = + rulesByObjectProperty.get(lookupKey(objectName, null)) || + rulesByObjectProperty.get(lookupKey(objectName, propertyName)); if (failingRule) checkNotInsideIfStatementAndReport( context, diff --git a/src/rules-lookup.ts b/src/rules-lookup.ts new file mode 100644 index 00000000..4df95b38 --- /dev/null +++ b/src/rules-lookup.ts @@ -0,0 +1,35 @@ +import { AstMetadataApiWithTargetsResolver } from "./types"; + +// https://stackoverflow.com/q/49752151/25507 +type KeysOfType = keyof T & + { [P in keyof T]: T[P] extends TProp ? P : never }[keyof T]; + +export type RulesLookup = Map< + string | undefined, + AstMetadataApiWithTargetsResolver +>; + +export function lookupKey(...args: Array) { + return args.map((i) => (i == null ? null : i)).join("\0"); +} + +export function makeLookup( + rules: AstMetadataApiWithTargetsResolver[], + ...keys: Array< + KeysOfType, string> + > +) { + const lookup = new Map< + string | undefined, + AstMetadataApiWithTargetsResolver + >(); + // Iterate in inverse order to favor earlier rules in case of conflict. + for (let i = rules.length - 1; i >= 0; i--) { + const key = + keys.length === 1 + ? rules[i][keys[0]] + : lookupKey(...keys.map((k) => rules[i][k])); + lookup.set(key, rules[i]); + } + return lookup; +} diff --git a/src/rules/compat.ts b/src/rules/compat.ts index f4a0fda8..0cee447a 100644 --- a/src/rules/compat.ts +++ b/src/rules/compat.ts @@ -26,6 +26,7 @@ import { BrowsersListOpts, } from "../types"; import { nodes } from "../providers"; +import { RulesLookup, makeLookup } from "../rules-lookup"; type ESLint = { [astNodeTypeName: string]: (node: ESLintNode) => void; @@ -112,18 +113,26 @@ type RulesFilteredByTargets = { ExpressionStatement: AstMetadataApiWithTargetsResolver[]; }; +type RuleLookupsSet = { + CallExpressionsByObject: RulesLookup; + NewExpressionsByObject: RulesLookup; + ExpressionStatementsByObject: RulesLookup; + MemberExpressionsByProtoChainId: RulesLookup; + MemberExpressionsByObjectProperty: RulesLookup; +}; + /** * A small optimization that only lints APIs that are not supported by targeted browsers. * For example, if the user is targeting chrome 50, which supports the fetch API, it is * wasteful to lint calls to fetch. */ const getRulesForTargets = memoize( - (targetsJSON: string, lintAllEsApis: boolean): RulesFilteredByTargets => { - const result = { - CallExpression: [] as AstMetadataApiWithTargetsResolver[], - NewExpression: [] as AstMetadataApiWithTargetsResolver[], - MemberExpression: [] as AstMetadataApiWithTargetsResolver[], - ExpressionStatement: [] as AstMetadataApiWithTargetsResolver[], + (targetsJSON: string, lintAllEsApis: boolean): RuleLookupsSet => { + const rules: RulesFilteredByTargets = { + CallExpression: [], + NewExpression: [], + MemberExpression: [], + ExpressionStatement: [], }; const targets = JSON.parse(targetsJSON); @@ -131,10 +140,36 @@ const getRulesForTargets = memoize( .filter((node) => (lintAllEsApis ? true : node.kind !== "es")) .forEach((node) => { if (!node.getUnsupportedTargets(node, targets).length) return; - result[node.astNodeType].push(node); + rules[node.astNodeType].push(node); }); - return result; + const expressionStatementRules = [ + ...rules.MemberExpression, + ...rules.CallExpression, + ]; + const memberExpressionRules = [ + ...rules.MemberExpression, + ...rules.CallExpression, + ...rules.NewExpression, + ]; + + return { + CallExpressionsByObject: makeLookup(rules.CallExpression, "object"), + NewExpressionsByObject: makeLookup(rules.NewExpression, "object"), + ExpressionStatementsByObject: makeLookup( + expressionStatementRules, + "object" + ), + MemberExpressionsByProtoChainId: makeLookup( + memberExpressionRules, + "protoChainId" + ), + MemberExpressionsByObjectProperty: makeLookup( + memberExpressionRules, + "object", + "property" + ), + }; } ); @@ -220,32 +255,29 @@ export default { null, context, handleFailingRule, - targetedRules.CallExpression, + targetedRules.CallExpressionsByObject, sourceCode ), NewExpression: lintNewExpression.bind( null, context, handleFailingRule, - targetedRules.NewExpression, + targetedRules.NewExpressionsByObject, sourceCode ), ExpressionStatement: lintExpressionStatement.bind( null, context, handleFailingRule, - [...targetedRules.MemberExpression, ...targetedRules.CallExpression], + targetedRules.ExpressionStatementsByObject, sourceCode ), MemberExpression: lintMemberExpression.bind( null, context, handleFailingRule, - [ - ...targetedRules.MemberExpression, - ...targetedRules.CallExpression, - ...targetedRules.NewExpression, - ], + targetedRules.MemberExpressionsByProtoChainId, + targetedRules.MemberExpressionsByObjectProperty, sourceCode ), // Keep track of all the defined variables. Do not report errors for nodes that are not defined diff --git a/test/e2e.spec.ts b/test/e2e.spec.ts index d95be72c..4df4980c 100644 --- a/test/e2e.spec.ts +++ b/test/e2e.spec.ts @@ -542,7 +542,7 @@ ruleTester.run("compat", rule, { settings: { browsers: ["ie 10"] }, errors: [ { - message: "Promise.resolve() is not supported in IE 10", + message: "Promise is not supported in IE 10", type: "MemberExpression", }, ], @@ -552,7 +552,7 @@ ruleTester.run("compat", rule, { settings: { browsers: ["ie 10"] }, errors: [ { - message: "Promise.all() is not supported in IE 10", + message: "Promise is not supported in IE 10", type: "MemberExpression", }, ], @@ -562,7 +562,7 @@ ruleTester.run("compat", rule, { settings: { browsers: ["ie 10"] }, errors: [ { - message: "Promise.race() is not supported in IE 10", + message: "Promise is not supported in IE 10", type: "MemberExpression", }, ], @@ -572,7 +572,7 @@ ruleTester.run("compat", rule, { settings: { browsers: ["ie 10"] }, errors: [ { - message: "Promise.reject() is not supported in IE 10", + message: "Promise is not supported in IE 10", type: "MemberExpression", }, ],