UNPKG

eslint-plugin-compat

Version:
262 lines (260 loc) 10.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.lintCallExpression = lintCallExpression; exports.lintNewExpression = lintNewExpression; exports.lintExpressionStatement = lintExpressionStatement; exports.lintLiteral = lintLiteral; exports.lintMemberExpression = lintMemberExpression; exports.reverseTargetMappings = reverseTargetMappings; exports.determineTargetsFromConfig = determineTargetsFromConfig; exports.parseBrowsersListVersion = parseBrowsersListVersion; /* eslint no-nested-ternary: off */ const browserslist_1 = __importDefault(require("browserslist")); const globals_1 = __importDefault(require("globals")); const constants_1 = require("./constants"); /* 3) Figures out which browsers user is targeting - Uses browserslist config and/or targets defined eslint config to discover this - For every API ecnountered during traversal, gets compat record for that - Protochain (e.g. 'document.querySelector') - All of the rules have compatibility info attached to them - Each API is given to versioning.ts with compatibility info */ function isInsideIfStatement(node, sourceCode, context) { // Handle both ESLint 8 and 9 - getAncestors moved from context to sourceCode // eslint-disable-next-line @typescript-eslint/no-explicit-any let ancestors; if ("getAncestors" in sourceCode) { // @ts-expect-error - ESLint 9+ uses sourceCode.getAncestors ancestors = sourceCode?.getAncestors?.(node); } else { // ESLint 8 uses context.getAncestors - cast to any for compatibility // eslint-disable-next-line @typescript-eslint/no-explicit-any ancestors = context.getAncestors?.(); } // eslint-disable-next-line @typescript-eslint/no-explicit-any return ancestors?.some((ancestor) => { return ancestor.type === "IfStatement"; }); } function checkNotInsideIfStatementAndReport(context, handleFailingRule, failingRule, sourceCode, node) { if (context.settings?.ignoreConditionalChecks === true || !isInsideIfStatement(node, sourceCode, context)) { handleFailingRule(failingRule, node); } } function lintCallExpression(context, handleFailingRule, rulesMap, sourceCode, node) { if (!node.callee) return; const calleeName = node.callee.name; if (!calleeName) return; const failingRule = rulesMap.get(calleeName); if (failingRule) checkNotInsideIfStatementAndReport(context, handleFailingRule, failingRule, sourceCode, node); } function lintNewExpression(context, handleFailingRule, rulesMap, sourceCode, node) { if (!node.callee) return; const calleeName = node.callee.name; if (!calleeName) return; const failingRule = rulesMap.get(calleeName); if (failingRule) checkNotInsideIfStatementAndReport(context, handleFailingRule, failingRule, sourceCode, node); } function lintExpressionStatement(context, handleFailingRule, rulesMap, sourceCode, node) { if (!node?.expression?.name) return; const failingRule = rulesMap.get(node.expression.name); if (failingRule) checkNotInsideIfStatementAndReport(context, handleFailingRule, failingRule, sourceCode, node); } function checkRegexpLiteral(node) { return (node.type === constants_1.AstNodeTypes.Literal && (!!node.regex || node.parent?.callee?.name === "RegExp")); } function lintLiteral(context, handleFailingRule, rulesMap, sourceCode, node) { const isRegexpLiteral = checkRegexpLiteral(node); if (!isRegexpLiteral) return; for (const [syntax, rule] of rulesMap) { if (node.raw.includes(syntax)) { handleFailingRule(rule, node); return; } } } function isStringLiteral(node) { return node.type === constants_1.AstNodeTypes.Literal && typeof node.value === "string"; } function protoChainFromMemberExpression(node) { if (!node.object) return [node.name]; const protoChain = (() => { if (node.object.type === "NewExpression" || node.object.type === "CallExpression") { return protoChainFromMemberExpression(node.object.callee); } else if (node.object.type === "ArrayExpression") { return ["Array"]; } else if (isStringLiteral(node.object)) { return ["String"]; } else { return protoChainFromMemberExpression(node.object); } })(); return [...protoChain, node.property.name]; } const browserGlobals = new Set(Object.keys(globals_1.default.browser)); /** * Secondary lookup for built-in `obj.prop` when the map was keyed with different * casing for `object` (e.g. `Document` in metadata vs `document` in the AST). The * property name must still match the source exactly. */ function findMemberRuleByGlobalObjectCasing(rulesMap, objectName, propertyName) { for (const [k, rule] of rulesMap) { const dot = k.indexOf("."); if (dot === -1) continue; const kObj = k.slice(0, dot); const kProp = k.slice(dot + 1); if (kObj.toLowerCase() === objectName.toLowerCase() && kProp === propertyName) { return rule; } } return undefined; } function lintMemberExpression(context, handleFailingRule, rulesMap, sourceCode, node) { if (!node.object || !node.property) return; if (!node.object.name || node.object.name === "window" || node.object.name === "globalThis") { const rawProtoChain = protoChainFromMemberExpression(node); const [firstObj] = rawProtoChain; const protoChain = firstObj === "window" || firstObj === "globalThis" ? rawProtoChain.slice(1) : rawProtoChain; const protoChainId = protoChain.join("."); const failingRule = rulesMap.get(protoChainId); if (failingRule) { checkNotInsideIfStatementAndReport(context, handleFailingRule, failingRule, sourceCode, node); } } else { const objectName = node.object.name; const propertyName = node.property.name; if (!objectName || !propertyName) return; const isBrowserGlobal = browserGlobals.has(objectName); let failingRule = rulesMap.get(`${objectName}.${propertyName}`) ?? rulesMap.get(objectName); if (!failingRule && isBrowserGlobal) { failingRule = findMemberRuleByGlobalObjectCasing(rulesMap, objectName, propertyName); } if (failingRule && !isBrowserGlobal && failingRule.object !== objectName) { failingRule = undefined; } if (failingRule) checkNotInsideIfStatementAndReport(context, handleFailingRule, failingRule, sourceCode, node); } } function reverseTargetMappings(targetMappings) { const reversedEntries = Object.entries(targetMappings).map((entry) => entry.reverse()); return Object.fromEntries(reversedEntries); } /** * Determine the targets based on the browserslist config object * Get the targets from the eslint config and merge them with targets in browserslist config * Eslint target config will be deprecated in 4.0.0 * * @param configPath - The file or a directory path to look for the browserslist config file */ function determineTargetsFromConfig(configPath, config, browserslistOptsFromConfig) { const browserslistOpts = { path: configPath, ...browserslistOptsFromConfig }; const eslintTargets = (() => { // Get targets from eslint settings if (Array.isArray(config) || typeof config === "string") { return (0, browserslist_1.default)(config, browserslistOpts); } if (config && typeof config === "object") { return (0, browserslist_1.default)([...(config.production || []), ...(config.development || [])], browserslistOpts); } return []; })(); if (browserslist_1.default.findConfig(configPath)) { // If targets are defined in ESLint and browerslist configs, merge the targets together if (eslintTargets.length) { const browserslistTargets = (0, browserslist_1.default)(undefined, browserslistOpts); return Array.from(new Set(eslintTargets.concat(browserslistTargets))); } } else if (eslintTargets.length) { return eslintTargets; } // Get targets fron browserslist configs return (0, browserslist_1.default)(undefined, browserslistOpts); } /** * Parses the versions that are given by browserslist. They're * * ```ts * parseBrowsersListVersion(['chrome 50']) * * { * target: 'chrome', * parsedVersion: 50, * version: '50' * } * ``` * @param targetslist - List of targest from browserslist api * @returns - The lowest version version of each target */ function parseBrowsersListVersion(targetslist) { return ( // Sort the targets by target name and then version number in ascending order targetslist .map((e) => { const [target, version] = e.split(" "); const parsedVersion = (() => { if (typeof version === "number") return version; if (version === "all") return 0; return version.includes("-") ? parseFloat(version.split("-")[0]) : parseFloat(version); })(); return { target, version, parsedVersion, }; }) // Sort the targets by target name and then version number in descending order // ex. [a@3, b@3, a@1] => [a@3, a@1, b@3] .sort((a, b) => { if (b.target === a.target) { // If any version === 'all', return 0. The only version of op_mini is 'all' // Otherwise, compare the versions return typeof b.parsedVersion === "string" || typeof a.parsedVersion === "string" ? 0 : b.parsedVersion - a.parsedVersion; } return b.target > a.target ? 1 : -1; }) // First last target always has the latest version .filter((e, i, items) => // Check if the current target is the last of its kind. // If it is, then it's the most recent version. i + 1 === items.length || e.target !== items[i + 1].target)); }