UNPKG

eslint-plugin-svelte

Version:
498 lines (497 loc) 16.2 kB
import * as eslintUtils from '@eslint-community/eslint-utils'; import { voidElements, svgElements, mathmlElements } from './element-types.js'; /** * Checks whether or not the tokens of two given nodes are same. * @param left A node 1 to compare. * @param right A node 2 to compare. * @param sourceCode The ESLint source code object. * @returns the source code for the given node. */ export function equalTokens(left, right, sourceCode) { const tokensL = sourceCode.getTokens(left); const tokensR = sourceCode.getTokens(right); if (tokensL.length !== tokensR.length) { return false; } for (let i = 0; i < tokensL.length; ++i) { if (tokensL[i].type !== tokensR[i].type || tokensL[i].value !== tokensR[i].value) { return false; } } return true; } /** * Get the value of a given node if it's a literal or a template literal. */ export function getStringIfConstant(node) { if (node.type === 'Literal') { if (typeof node.value === 'string') return node.value; } else if (node.type === 'TemplateLiteral') { let str = ''; const quasis = [...node.quasis]; const expressions = [...node.expressions]; let quasi, expr; while ((quasi = quasis.shift())) { str += quasi.value.cooked; expr = expressions.shift(); if (expr) { const exprStr = getStringIfConstant(expr); if (exprStr == null) { return null; } str += exprStr; } } return str; } else if (node.type === 'BinaryExpression') { if (node.operator === '+') { const left = getStringIfConstant(node.left); if (left == null) { return null; } const right = getStringIfConstant(node.right); if (right == null) { return null; } return left + right; } } return null; } /** * Check if it need parentheses. */ export function needParentheses(node, kind) { if (node.type === 'ArrowFunctionExpression' || node.type === 'AssignmentExpression' || node.type === 'BinaryExpression' || node.type === 'ConditionalExpression' || node.type === 'LogicalExpression' || node.type === 'SequenceExpression' || node.type === 'UnaryExpression' || node.type === 'UpdateExpression') return true; if (kind === 'logical') { return node.type === 'FunctionExpression'; } return false; } /** Checks whether the given node is the html element node or <svelte:element> node. */ export function isHTMLElementLike(node) { if (node.type !== 'SvelteElement') { return false; } switch (node.kind) { case 'html': return true; case 'special': return node.name.name === 'svelte:element'; default: return false; } } /** * Find the attribute from the given element node */ export function findAttribute(node, name) { const startTag = node.type === 'SvelteStartTag' ? node : node.startTag; for (const attr of startTag.attributes) { if (attr.type === 'SvelteAttribute') { if (attr.key.name === name) { return attr; } } } return null; } /** * Find the shorthand attribute from the given element node */ export function findShorthandAttribute(node, name) { const startTag = node.type === 'SvelteStartTag' ? node : node.startTag; for (const attr of startTag.attributes) { if (attr.type === 'SvelteShorthandAttribute') { if (attr.key.name === name) { return attr; } } } return null; } /** * Find the bind directive from the given element node */ export function findBindDirective(node, name) { const startTag = node.type === 'SvelteStartTag' ? node : node.startTag; for (const attr of startTag.attributes) { if (attr.type === 'SvelteDirective') { if (attr.kind === 'Binding' && attr.key.name.name === name) { return attr; } } } return null; } /** * Get the static attribute value from given attribute */ export function getStaticAttributeValue(node) { let str = ''; for (const value of node.value) { if (value.type === 'SvelteLiteral') { str += value.value; } else { return null; } } return str; } /** * Get the static attribute value from given attribute */ export function getLangValue(node) { const langAttr = findAttribute(node, 'lang'); return langAttr && getStaticAttributeValue(langAttr); } /** * Find the variable of a given name. */ export function findVariable(context, node) { const initialScope = eslintUtils.getInnermostScope(getScope(context, node), node); const variable = eslintUtils.findVariable(initialScope, node); if (variable) { return variable; } if (!node.name.startsWith('$')) { return variable; } // Remove the $ and search for the variable again, as it may be a store access variable. return eslintUtils.findVariable(initialScope, node.name.slice(1)); } /** * Iterate the identifiers of a given pattern node. */ export function* iterateIdentifiers(node) { const buffer = [node]; let pattern; while ((pattern = buffer.shift())) { if (pattern.type === 'Identifier') { yield pattern; } else if (pattern.type === 'ArrayPattern') { for (const element of pattern.elements) { if (element) { buffer.push(element); } } } else if (pattern.type === 'ObjectPattern') { for (const property of pattern.properties) { if (property.type === 'Property') { buffer.push(property.value); } else if (property.type === 'RestElement') { buffer.push(property); } } } else if (pattern.type === 'AssignmentPattern') { buffer.push(pattern.left); } else if (pattern.type === 'RestElement') { buffer.push(pattern.argument); } else if (pattern.type === 'MemberExpression') { // noop } } } /** * Gets the scope for the current node */ export function getScope(context, currentNode) { const scopeManager = context.sourceCode.scopeManager; let node = currentNode; for (; node; node = node.parent || null) { const scope = scopeManager.acquire(node, false); if (scope) { if (scope.type === 'function-expression-name') { return scope.childScopes[0]; } return scope; } } return scopeManager.scopes[0]; } /** Get the parent node from the given node */ export function getParent(node) { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore return node.parent || null; } /** Get the quote and range from given attribute values */ export function getAttributeValueQuoteAndRange(attr, sourceCode) { const valueTokens = getAttributeValueRangeTokens(attr, sourceCode); if (valueTokens == null) { return null; } const { firstToken: valueFirstToken, lastToken: valueLastToken } = valueTokens; const eqToken = sourceCode.getTokenAfter(attr.key); if (!eqToken || eqToken.value !== '=' || valueFirstToken.range[0] < eqToken.range[1]) { // invalid return null; } const beforeTokens = sourceCode.getTokensBetween(eqToken, valueFirstToken); if (beforeTokens.length === 0) { return { quote: 'unquoted', range: [valueFirstToken.range[0], valueLastToken.range[1]], firstToken: valueFirstToken, lastToken: valueLastToken }; } else if (beforeTokens.length > 1 || (beforeTokens[0].value !== '"' && beforeTokens[0].value !== "'")) { // invalid return null; } const beforeToken = beforeTokens[0]; const afterToken = sourceCode.getTokenAfter(valueLastToken); if (!afterToken || afterToken.value !== beforeToken.value) { // invalid return null; } return { quote: beforeToken.value === '"' ? 'double' : 'single', range: [beforeToken.range[0], afterToken.range[1]], firstToken: beforeToken, lastToken: afterToken }; } /** Get the mustache tokens from given node */ export function getMustacheTokens(node, sourceCode) { if (isWrappedInBraces(node)) { const openToken = sourceCode.getFirstToken(node); const closeToken = sourceCode.getLastToken(node); return { openToken, closeToken }; } if (node.expression == null) { return null; } if (node.key.range[0] <= node.expression.range[0] && node.expression.range[1] <= node.key.range[1]) { // shorthand return null; } let openToken = sourceCode.getTokenBefore(node.expression); let closeToken = sourceCode.getTokenAfter(node.expression); while (openToken && closeToken && eslintUtils.isOpeningParenToken(openToken) && eslintUtils.isClosingParenToken(closeToken)) { openToken = sourceCode.getTokenBefore(openToken); closeToken = sourceCode.getTokenAfter(closeToken); } if (!openToken || !closeToken || eslintUtils.isNotOpeningBraceToken(openToken) || eslintUtils.isNotClosingBraceToken(closeToken)) { return null; } return { openToken, closeToken }; } function isWrappedInBraces(node) { return (node.type === 'SvelteMustacheTag' || node.type === 'SvelteShorthandAttribute' || node.type === 'SvelteSpreadAttribute' || node.type === 'SvelteDebugTag' || node.type === 'SvelteRenderTag'); } /** Get attribute key text */ export function getAttributeKeyText(node, context) { switch (node.type) { case 'SvelteAttribute': case 'SvelteShorthandAttribute': case 'SvelteGenericsDirective': return node.key.name; case 'SvelteStyleDirective': return `style:${node.key.name.name}`; case 'SvelteSpecialDirective': return node.kind; case 'SvelteDirective': { const dir = getDirectiveName(node); return `${dir}:${getSimpleNameFromNode(node.key.name, context)}${node.key.modifiers.length ? `|${node.key.modifiers.join('|')}` : ''}`; } default: throw new Error(`Unknown node type: ${ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- error node.type}`); } } /** Get directive name */ export function getDirectiveName(node) { switch (node.kind) { case 'Action': return 'use'; case 'Animation': return 'animate'; case 'Binding': return 'bind'; case 'Class': return 'class'; case 'EventHandler': return 'on'; case 'Let': return 'let'; case 'Transition': return node.intro && node.outro ? 'transition' : node.intro ? 'in' : 'out'; case 'Ref': return 'ref'; default: throw new Error('Unknown directive kind'); } } /** Get the value tokens from given attribute */ function getAttributeValueRangeTokens(attr, sourceCode) { if (attr.type === 'SvelteAttribute' || attr.type === 'SvelteStyleDirective') { if (!attr.value.length) { return null; } const first = attr.value[0]; const last = attr.value[attr.value.length - 1]; return { firstToken: sourceCode.getFirstToken(first), lastToken: sourceCode.getLastToken(last) }; } const tokens = getMustacheTokens(attr, sourceCode); if (!tokens) { return null; } return { firstToken: tokens.openToken, lastToken: tokens.closeToken }; } /** * Extract all class names used in a HTML element attribute. */ export function findClassesInAttribute(attribute) { if (attribute.type === 'SvelteAttribute' && attribute.key.name === 'class') { return attribute.value.flatMap((value) => value.type === 'SvelteLiteral' ? value.value.trim().split(/\s+/u) : []); } if (attribute.type === 'SvelteDirective' && attribute.kind === 'Class') { return [attribute.key.name.name]; } return []; } /** * Returns name of SvelteElement */ export function getNodeName(node) { return getSimpleNameFromNode(node.name); } /** * Returns true if element is known void element * {@link https://developer.mozilla.org/en-US/docs/Glossary/Empty_element} */ export function isVoidHtmlElement(node) { return voidElements.includes(getNodeName(node)); } /** * Returns true if element is known foreign (SVG or MathML) element */ export function isForeignElement(node) { return svgElements.includes(getNodeName(node)) || mathmlElements.includes(getNodeName(node)); } export function isSvgElement(node) { return svgElements.includes(getNodeName(node)); } export function isMathMLElement(node) { return mathmlElements.includes(getNodeName(node)); } /** Checks whether the given identifier node is used as an expression. */ export function isExpressionIdentifier(node) { const parent = node.parent; if (!parent) { return true; } if (parent.type === 'MemberExpression') { return !parent.computed || parent.property !== node; } if (parent.type === 'Property' || parent.type === 'MethodDefinition' || parent.type === 'PropertyDefinition') { return !parent.computed || parent.key !== node; } if (parent.type === 'FunctionDeclaration' || parent.type === 'FunctionExpression' || parent.type === 'ClassDeclaration' || parent.type === 'ClassExpression') { return parent.id !== node; } if (parent.type === 'LabeledStatement' || parent.type === 'BreakStatement' || parent.type === 'ContinueStatement') { return parent.label !== node; } if (parent.type === 'MetaProperty') { return parent.property !== node; } if (parent.type === 'ImportSpecifier') { return parent.imported !== node; } if (parent.type === 'ExportSpecifier') { return parent.exported !== node; } return true; } /** Get simple name from give node */ function getSimpleNameFromNode(node, context) { if (node.type === 'Identifier' || node.type === 'SvelteName') { return node.name; } if (node.type === 'SvelteMemberExpressionName' || (node.type === 'MemberExpression' && !node.computed)) { return `${getSimpleNameFromNode(node.object, context)}.${getSimpleNameFromNode(node.property, context)}`; } // No nodes other than those listed above are currently expected to be used in names. if (!context) { throw new Error('Rule context is required'); } return context.sourceCode.getText(node); } /** * Finds the variable for a given name in the specified node's scope. * Also determines if the replacement name is already in use. * * If the `name` is set to null, this assumes you're adding a new variable * and reports if it is already in use. */ export function findVariableForReplacement(context, node, name, replacementName) { const scope = getScope(context, node); let variable = null; for (const ref of scope.references) { if (ref.identifier.name === replacementName) { return { hasConflict: true, variable: null }; } } for (const v of scope.variables) { if (v.name === replacementName) { return { hasConflict: true, variable: null }; } if (v.name === name) { variable = v; } } return { hasConflict: false, variable }; }