UNPKG

eslint-plugin-solid

Version:
218 lines (206 loc) 7.44 kB
/** * FIXME: remove this comments and import when below issue is fixed. * This import is necessary for type generation due to a bug in the TypeScript compiler. * See: https://github.com/microsoft/TypeScript/issues/42873 */ // eslint-disable-next-line @typescript-eslint/no-unused-vars import type { TSESLint } from "@typescript-eslint/utils"; import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils"; import { isDOMElementName, formatList, appendImports, insertImports } from "../utils"; import { getScope, getSourceCode } from "../compat"; const createRule = ESLintUtils.RuleCreator.withoutDocs; // Currently all of the control flow components are from 'solid-js'. const AUTO_COMPONENTS = ["Show", "For", "Index", "Switch", "Match"]; const SOURCE_MODULE = "solid-js"; /* * This rule is adapted from eslint-plugin-react's jsx-no-undef rule under * the MIT license. Thank you for your work! */ type MessageIds = "undefined" | "customDirectiveUndefined" | "autoImport"; type Options = [ { allowGlobals?: boolean; autoImport?: boolean; typescriptEnabled?: boolean; }? ]; export default createRule<Options, MessageIds>({ meta: { type: "problem", docs: { description: "Disallow references to undefined variables in JSX. Handles custom directives.", url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-no-undef.md", }, fixable: "code", schema: [ { type: "object", properties: { allowGlobals: { type: "boolean", description: "When true, the rule will consider the global scope when checking for defined components.", default: false, }, autoImport: { type: "boolean", description: 'Automatically import certain components from `"solid-js"` if they are undefined.', default: true, }, typescriptEnabled: { type: "boolean", description: "Adjusts behavior not to conflict with TypeScript's type checking.", default: false, }, }, additionalProperties: false, }, ], messages: { undefined: "'{{identifier}}' is not defined.", customDirectiveUndefined: "Custom directive '{{identifier}}' is not defined.", autoImport: "{{imports}} should be imported from '{{source}}'.", }, }, defaultOptions: [], create(context) { const allowGlobals = context.options[0]?.allowGlobals ?? false; const autoImport = context.options[0]?.autoImport !== false; const isTypeScriptEnabled = context.options[0]?.typescriptEnabled ?? false; const missingComponentsSet = new Set<string>(); /** * Compare an identifier with the variables declared in the scope * @param {ASTNode} node - Identifier or JSXIdentifier node * @returns {void} */ function checkIdentifierInJSX( node: T.Identifier | T.JSXIdentifier, { isComponent, isCustomDirective, }: { isComponent?: boolean; isCustomDirective?: boolean } = {} ) { let scope = getScope(context, node); const sourceCode = getSourceCode(context); const sourceType = sourceCode.ast.sourceType; const scopeUpperBound = !allowGlobals && sourceType === "module" ? "module" : "global"; const variables = [...scope.variables]; // Ignore 'this' keyword (also maked as JSXIdentifier when used in JSX) if (node.name === "this") { return; } while (scope.type !== scopeUpperBound && scope.type !== "global" && scope.upper) { scope = scope.upper; variables.push(...scope.variables); } if (scope.childScopes.length) { variables.push(...scope.childScopes[0].variables); // Temporary fix for babel-eslint if (scope.childScopes[0].childScopes.length) { variables.push(...scope.childScopes[0].childScopes[0].variables); } } if (variables.find((variable) => variable.name === node.name)) { return; } if ( isComponent && autoImport && AUTO_COMPONENTS.includes(node.name) && !missingComponentsSet.has(node.name) ) { // track which names are undefined missingComponentsSet.add(node.name); } else if (isCustomDirective) { context.report({ node, messageId: "customDirectiveUndefined", data: { identifier: node.name, }, }); } else if (!isTypeScriptEnabled) { context.report({ node, messageId: "undefined", data: { identifier: node.name, }, }); } } return { JSXOpeningElement(node) { let n: T.Node | undefined; switch (node.name.type) { case "JSXIdentifier": if (!isDOMElementName(node.name.name)) { checkIdentifierInJSX(node.name, { isComponent: true }); } break; case "JSXMemberExpression": n = node.name; do { n = (n as any).object; } while (n && n.type !== "JSXIdentifier"); if (n) { checkIdentifierInJSX(n); } break; default: break; } }, "JSXAttribute > JSXNamespacedName": (node: T.JSXNamespacedName) => { // <Element use:X /> applies the `X` custom directive to the element, where `X` must be an identifier in scope. if ( node.namespace?.type === "JSXIdentifier" && node.namespace.name === "use" && node.name?.type === "JSXIdentifier" ) { checkIdentifierInJSX(node.name, { isCustomDirective: true }); } }, "Program:exit": (programNode: T.Program) => { // add in any auto import components used in the program const missingComponents = Array.from(missingComponentsSet.values()); if (autoImport && missingComponents.length) { const importNode = programNode.body.find( (n) => n.type === "ImportDeclaration" && n.importKind !== "type" && n.source.type === "Literal" && n.source.value === SOURCE_MODULE ) as T.ImportDeclaration | undefined; if (importNode) { context.report({ node: importNode, messageId: "autoImport", data: { imports: formatList(missingComponents), // "Show, For, and Switch" source: SOURCE_MODULE, }, fix: (fixer) => { return appendImports(fixer, getSourceCode(context), importNode, missingComponents); }, }); } else { context.report({ node: programNode, messageId: "autoImport", data: { imports: formatList(missingComponents), source: SOURCE_MODULE, }, fix: (fixer) => { // insert `import { missing, identifiers } from "solid-js"` at top of module return insertImports(fixer, getSourceCode(context), "solid-js", missingComponents); }, }); } } }, }; }, });