UNPKG

eslint-plugin-compat

Version:
204 lines (203 loc) 9.04 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const find_up_1 = __importDefault(require("find-up")); const fs_1 = __importDefault(require("fs")); const lodash_memoize_1 = __importDefault(require("lodash.memoize")); const path_1 = __importDefault(require("path")); const helpers_1 = require("../helpers"); // will be deprecated and introduced to this file const providers_1 = require("../providers"); function getName(node) { switch (node.type) { case "NewExpression": { return node.callee.name; } case "MemberExpression": { return node.object.name; } case "ExpressionStatement": { return node.expression.name; } case "CallExpression": { return node.callee.name; } case "Literal": { return node.type; } default: throw new Error("not found"); } } function generateErrorName(rule) { if (rule.name) return rule.name; if (rule.property) return `${rule.object}.${rule.property}()`; return rule.object; } const getPolyfillSet = (0, lodash_memoize_1.default)((polyfillArrayJSON) => new Set(JSON.parse(polyfillArrayJSON))); function isPolyfilled(context, rule) { if (!context.settings?.polyfills) return false; const polyfills = getPolyfillSet(JSON.stringify(context.settings.polyfills)); return ( // v2 allowed users to select polyfills based off their caniuseId. This is polyfills.has(rule.id) || // no longer supported. Keeping this here to avoid breaking changes. polyfills.has(rule.protoChainId) || // Check if polyfill is provided (ex. `Promise.all`) polyfills.has(rule.protoChain[0]) // Check if entire API is polyfilled (ex. `Promise`) ); } const babelConfigs = [ "babel.config.json", "babel.config.js", "babel.config.cjs", ".babelrc", ".babelrc.json", ".babelrc.js", ".babelrc.cjs", ]; /** * Determine if a user has a babel config, which we use to infer if the linted code is polyfilled. * Memoized by directory so multiple files in the same project reuse the result. */ const isUsingTranspiler = (0, lodash_memoize_1.default)((filePath) => { const dir = path_1.default.dirname(filePath); const configPath = find_up_1.default.sync(babelConfigs, { cwd: dir, }); if (configPath) return true; const pkgPath = find_up_1.default.sync("package.json", { cwd: dir, }); // Check if babel property exists if (pkgPath) { const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath).toString()); return !!pkg.babel; } return false; }, (filePath) => path_1.default.resolve(path_1.default.dirname(filePath))); /** * 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 = (0, lodash_memoize_1.default)((targetsJSON, lintAllEsApis) => { const result = { CallExpression: [], NewExpression: [], MemberExpression: [], ExpressionStatement: [], Literal: [], }; const targets = JSON.parse(targetsJSON); providers_1.nodes .filter((node) => (lintAllEsApis ? true : node.kind !== "es")) .forEach((node) => { if (!node.getUnsupportedTargets(node, targets).length) return; result[node.astNodeType].push(node); }); return result; }); exports.default = { meta: { docs: { description: "Ensure cross-browser API compatibility", category: "Compatibility", url: "https://github.com/amilajack/eslint-plugin-compat/blob/main/docs/rules/compat.md", recommended: true, }, type: "problem", schema: [{ type: "string" }], }, create(context) { const sourceCode = // eslint-disable-next-line @typescript-eslint/no-explicit-any context.sourceCode ?? context.getSourceCode(); // Determine lowest targets from browserslist config, which reads user's // package.json config section. Use config from eslintrc for testing purposes const browserslistConfig = context.settings?.browsers || context.settings?.targets || context.options[0]; if (!context.settings?.browserslistOpts && // @ts-expect-error Checking for accidental misspellings context.settings.browsersListOpts) { // eslint-disable-next-line -- CLI console.error('Please ensure you spell `browserslistOpts` with a lowercase "l"!'); } const browserslistOpts = context.settings?.browserslistOpts; const browserslistDir = // eslint-disable-next-line @typescript-eslint/no-explicit-any context.filename ?? context.getFilename(); const lintAllEsApis = context.settings?.lintAllEsApis === true || // Attempt to infer polyfilling of ES APIs from babel config (!context.settings?.polyfills?.includes("es:all") && !isUsingTranspiler(browserslistDir)); const browserslistTargets = (0, helpers_1.parseBrowsersListVersion)((0, helpers_1.determineTargetsFromConfig)(browserslistDir, browserslistConfig, browserslistOpts)); // Stringify to support memoization; browserslistConfig is always an array of new objects. const targetedRules = getRulesForTargets(JSON.stringify(browserslistTargets), lintAllEsApis); const errors = []; // Cache getUnsupportedTargets per rule; targets are fixed for this context. const unsupportedTargetsByRule = new Map(); const getUnsupportedTargetsMessage = (rule) => { let message = unsupportedTargetsByRule.get(rule.id); if (message === undefined) { message = rule .getUnsupportedTargets(rule, browserslistTargets) .join(", "); unsupportedTargetsByRule.set(rule.id, message); } return message; }; const handleFailingRule = (node, eslintNode) => { if (isPolyfilled(context, node)) return; errors.push({ node: eslintNode, message: [ generateErrorName(node), "is not supported in", getUnsupportedTargetsMessage(node), ].join(" "), }); }; const identifiers = new Set(); return { CallExpression: helpers_1.lintCallExpression.bind(null, context, handleFailingRule, targetedRules.CallExpression, sourceCode), NewExpression: helpers_1.lintNewExpression.bind(null, context, handleFailingRule, targetedRules.NewExpression, sourceCode), ExpressionStatement: helpers_1.lintExpressionStatement.bind(null, context, handleFailingRule, [...targetedRules.MemberExpression, ...targetedRules.CallExpression], sourceCode), MemberExpression: helpers_1.lintMemberExpression.bind(null, context, handleFailingRule, [ ...targetedRules.MemberExpression, ...targetedRules.CallExpression, ...targetedRules.NewExpression, ], sourceCode), Literal: helpers_1.lintLiteral.bind(null, context, handleFailingRule, targetedRules.Literal, sourceCode), // Keep track of all the defined variables. Do not report errors for nodes that are not defined Identifier(node) { if (node.parent) { const { type } = node.parent; if (type === "Property" || // ex. const { Set } = require('immutable'); type === "FunctionDeclaration" || // ex. function Set() {} type === "VariableDeclarator" || // ex. const Set = () => {} type === "ClassDeclaration" || // ex. class Set {} type === "ImportDefaultSpecifier" || // ex. import Set from 'set'; type === "ImportSpecifier" || // ex. import {Set} from 'set'; type === "ImportDeclaration" // ex. import {Set} from 'set'; ) { identifiers.add(node.name); } } }, "Program:exit": () => { // Get a map of all the variables defined in the root scope (not the global scope) // const variablesMap = context.getScope().childScopes.map(e => e.set)[0]; errors .filter((error) => !identifiers.has(getName(error.node))) .forEach((node) => context.report(node)); }, }; }, };