UNPKG

eslint-plugin-svelte

Version:
519 lines (518 loc) 18.9 kB
import * as compiler from 'svelte/compiler'; import { decode } from '@jridgewell/sourcemap-codec'; import { LinesAndColumns } from '../../utils/lines-and-columns.js'; import { hasTypeScript, transform as transformWithTypescript } from './transform/typescript.js'; import { transform as transformWithBabel } from './transform/babel.js'; import { transform as transformWithPostCSS } from './transform/postcss.js'; import { transform as transformWithSass } from './transform/sass.js'; import { transform as transformWithLess } from './transform/less.js'; import { transform as transformWithStylus } from './transform/stylus.js'; import { getSvelteIgnoreItems } from './ignore-comment.js'; import { extractLeadingComments } from './extract-leading-comments.js'; import { findAttribute, getLangValue } from '../../utils/ast-utils.js'; import path from 'path'; import fs from 'fs'; import semver from 'semver'; const STYLE_TRANSFORMS = { postcss: transformWithPostCSS, pcss: transformWithPostCSS, scss: (node, text, context) => transformWithSass(node, text, context, 'scss'), sass: (node, text, context) => transformWithSass(node, text, context, 'sass'), less: transformWithLess, stylus: transformWithStylus, styl: transformWithStylus }; const CSS_WARN_CODES = new Set([ 'css-unused-selector', 'css_unused_selector', 'css-invalid-global', 'css-invalid-global-selector' ]); const cacheAll = new WeakMap(); /** * Get svelte compile warnings */ export function getSvelteCompileWarnings(context) { const sourceCode = context.sourceCode; const cache = cacheAll.get(sourceCode.ast); if (cache) { return cache; } const result = getSvelteCompileWarningsWithoutCache(context); cacheAll.set(sourceCode.ast, result); return result; } /** * Get svelte compile warnings */ function getSvelteCompileWarningsWithoutCache(context) { const sourceCode = context.sourceCode; // Process for styles const styleElementsWithNotCSS = [...extractStyleElementsWithLangOtherThanCSS(context)]; const stripStyleElements = []; const transformResults = []; for (const style of styleElementsWithNotCSS) { const transform = STYLE_TRANSFORMS[style.lang]; if (transform) { const result = transform(style.node, context.sourceCode.text, context); if (result) { transformResults.push(result); continue; } } stripStyleElements.push(style.node); } const stripStyleTokens = stripStyleElements.flatMap((e) => e.children); const ignoreComments = getSvelteIgnoreItems(context).filter((item) => item.code != null); const text = buildStrippedText(context, ignoreComments, stripStyleTokens); transformResults.push(...transformScripts(context, text)); if (!transformResults.length) { const warnings = getWarningsFromCode(text, context); return { ...processIgnore(warnings.warnings, warnings.kind, stripStyleElements, ignoreComments, context), kind: warnings.kind, stripStyleElements }; } class RemapContext { constructor() { this.originalStart = 0; this.code = ''; this.locs = null; this.mapIndexes = []; } appendOriginal(endIndex) { const codeStart = this.code.length; const start = this.originalStart; this.code += text.slice(start, endIndex); this.originalStart = endIndex; const offset = start - codeStart; this.mapIndexes.push({ range: [codeStart, this.code.length], remap(index) { return index + offset; } }); } postprocess() { this.appendOriginal(text.length); return this.code; } appendTranspile(output) { const endIndex = output.inputRange[1]; const codeStart = this.code.length; const start = this.originalStart; const inputText = text.slice(start, endIndex); const outputText = `${output.output}\n`; this.code += outputText; this.originalStart = endIndex; let outputLocs = null; let inputLocs = null; let decoded = null; this.mapIndexes.push({ range: [codeStart, this.code.length], remap: (index) => { outputLocs = outputLocs ?? new LinesAndColumns(outputText); inputLocs = inputLocs ?? new LinesAndColumns(inputText); const outputCodePos = outputLocs.getLocFromIndex(index - codeStart); const inputCodePos = remapPosition(outputCodePos); return inputLocs.getIndexFromLoc(inputCodePos) + start; } }); /** Remapping source position */ function remapPosition(pos) { decoded = decoded ?? decode(output.mappings); const lineMaps = decoded[pos.line - 1]; if (!lineMaps?.length) { for (let line = pos.line - 1; line >= 0; line--) { const prevLineMaps = decoded[line]; if (prevLineMaps?.length) { const [, , sourceCodeLine, sourceCodeColumn] = prevLineMaps[prevLineMaps.length - 1]; return { line: sourceCodeLine + 1, column: sourceCodeColumn }; } } return { line: -1, column: -1 }; } for (let index = 0; index < lineMaps.length - 1; index++) { const [generateCodeColumn, , sourceCodeLine, sourceCodeColumn] = lineMaps[index]; if (generateCodeColumn <= pos.column && pos.column < lineMaps[index + 1][0]) { return { line: sourceCodeLine + 1, column: sourceCodeColumn + (pos.column - generateCodeColumn) }; } } const [generateCodeColumn, , sourceCodeLine, sourceCodeColumn] = lineMaps[lineMaps.length - 1]; return { line: sourceCodeLine + 1, column: sourceCodeColumn + (pos.column - generateCodeColumn) }; } } remapLocs(points) { const mapIndexes = this.mapIndexes; const locs = (this.locs = this.locs ?? new LinesAndColumns(this.code)); let start = undefined; let end = undefined; if (points.start) { const index = locs.getIndexFromLoc(points.start); const remapped = remapIndex(index); if (remapped) { start = { ...sourceCode.getLocFromIndex(remapped), character: remapped }; } } if (points.end) { const index = locs.getIndexFromLoc(points.end); const remapped = remapIndex(index - 1 /* include index */); if (remapped) { const character = remapped + 1; /* restore */ end = { ...sourceCode.getLocFromIndex(character), character }; } } return { start, end }; /** remap index */ function remapIndex(index) { for (const mapIndex of mapIndexes) { if (mapIndex.range[0] <= index && index < mapIndex.range[1]) { return mapIndex.remap(index); } } return null; } } } const remapContext = new RemapContext(); for (const result of transformResults.sort((a, b) => a.inputRange[0] - b.inputRange[0])) { remapContext.appendOriginal(result.inputRange[0]); remapContext.appendTranspile(result); } const code = remapContext.postprocess(); const baseWarnings = getWarningsFromCode(code, context); const warnings = []; for (const warn of baseWarnings.warnings) { let loc = null; /** Get re-mapped location */ // eslint-disable-next-line func-style -- ignore const getLoc = function getLoc() { if (loc) { return loc; } return (loc = remapContext.remapLocs(warn)); }; warnings.push({ code: warn.code, message: warn.message, get start() { return getLoc().start; }, get end() { return getLoc().end; } }); } return { ...processIgnore(warnings, baseWarnings.kind, stripStyleElements, ignoreComments, context), kind: baseWarnings.kind, stripStyleElements }; } /** * Extracts the style with the lang attribute other than CSS. */ function* extractStyleElementsWithLangOtherThanCSS(context) { const sourceCode = context.sourceCode; const root = sourceCode.ast; for (const node of root.body) { if (node.type === 'SvelteStyleElement') { const lang = getLangValue(node); if (lang != null && lang.toLowerCase() !== 'css') { yield { node, lang: lang.toLowerCase() }; } } } } /** * Build the text stripped of tokens that are not needed for compilation. */ function buildStrippedText(context, ignoreComments, stripStyleTokens) { const sourceCode = context.sourceCode; const baseText = sourceCode.text; const stripTokens = new Set([...ignoreComments.map((item) => item.token), ...stripStyleTokens]); if (!stripTokens.size) { return baseText; } let code = ''; let start = 0; for (const token of [...stripTokens].sort((a, b) => a.range[0] - b.range[0])) { code += baseText.slice(start, token.range[0]) + baseText.slice(...token.range).replace(/[^\t\n\r ]/g, ' '); start = token.range[1]; } code += baseText.slice(start); return code; } /** Returns the result of transforming the required script for the transform. */ function* transformScripts(context, text) { const transform = isUseTypeScript(context) ? hasTypeScript(context) ? transformWithTypescript : transformWithBabel : isUseBabel(context) ? transformWithBabel : null; const sourceCode = context.sourceCode; if (transform) { const root = sourceCode.ast; for (const node of root.body) { if (node.type === 'SvelteScriptElement') { const result = transform(node, text, context); if (result) { yield result; } } } } } function hasTagOption(program) { return program.body.some((body) => { if (body.type !== 'SvelteElement' || body.kind !== 'special') { return false; } if (body.name.name !== 'svelte:options') { return false; } return Boolean(findAttribute(body, 'tag')); }); } /** * Get compile warnings */ function getWarningsFromCode(code, context) { try { const result = compiler.compile(code, { generate: false, ...(semver.satisfies(compiler.VERSION, '>=4.0.0-0') ? { customElement: true } : hasTagOption(context.sourceCode.ast) ? { customElement: true } : {}) }); return { warnings: result.warnings, kind: 'warn' }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore } catch (e) { return { warnings: [ { code: e.code, message: e.message, start: e.start, end: e.end } ], kind: 'error' }; } } /** Ignore process */ function processIgnore(warnings, kind, stripStyleElements, ignoreComments, context) { if (kind === 'error') { return { warnings, unusedIgnores: ignoreComments }; } const sourceCode = context.sourceCode; const unusedIgnores = new Set(ignoreComments); const remainingWarning = new Set(warnings); for (const warning of warnings) { if (!warning.code) { continue; } let node = getWarningNode(warning); while (node) { for (const comment of extractLeadingComments(context, node).reverse()) { const ignoreItem = ignoreComments.find((item) => item.token === comment && (item.code === warning.code || item.codeForV5 === warning.code)); if (ignoreItem) { unusedIgnores.delete(ignoreItem); remainingWarning.delete(warning); break; } } node = getIgnoreParent(node); } } // Stripped styles are ignored from compilation and cannot determine css errors. for (const node of stripStyleElements) { for (const comment of extractLeadingComments(context, node).reverse()) { const ignoreItem = ignoreComments.find((item) => item.token === comment && (CSS_WARN_CODES.has(item.code) || CSS_WARN_CODES.has(item.codeForV5))); if (ignoreItem) { unusedIgnores.delete(ignoreItem); break; } } } return { warnings: [...remainingWarning], unusedIgnores: [...unusedIgnores] }; /** Get ignore target parent node */ function getIgnoreParent(node) { if (node.type !== 'SvelteElement' && node.type !== 'SvelteIfBlock' && node.type !== 'SvelteKeyBlock' && node.type !== 'SvelteEachBlock' && node.type !== 'SvelteAwaitBlock') { return null; } const parent = node.parent; if (parent.type === 'SvelteElseBlock') { return parent.parent; // SvelteIfBlock or SvelteEachBlock } if (parent.type === 'SvelteAwaitPendingBlock' || parent.type === 'SvelteAwaitThenBlock' || parent.type === 'SvelteAwaitCatchBlock') { return parent.parent; // SvelteAwaitBlock } if (parent.type !== 'SvelteElement' && parent.type !== 'SvelteIfBlock' && parent.type !== 'SvelteKeyBlock' && parent.type !== 'SvelteEachBlock' // && parent.type !== "SvelteAwaitBlock" ) { return null; } return parent; } /** Get warning node */ function getWarningNode(warning) { const indexes = getWarningIndexes(warning); if (indexes.start != null) { const node = getWarningTargetNodeFromIndex(indexes.start); if (node) { return node; } if (indexes.end != null) { const center = Math.floor(indexes.start + (indexes.end - indexes.start) / 2); return getWarningTargetNodeFromIndex(center); } } if (indexes.end != null) { return getWarningTargetNodeFromIndex(indexes.end); } return null; } /** * Get warning target node from the given index */ function getWarningTargetNodeFromIndex(index) { let targetNode = sourceCode.getNodeByRangeIndex(index); while (targetNode) { if (targetNode.type === 'SvelteElement' || targetNode.type === 'SvelteStyleElement') { return targetNode; } if (targetNode.parent) { if (targetNode.parent.type === 'Program' || targetNode.parent.type === 'SvelteScriptElement') { return targetNode; } } else { return null; } targetNode = targetNode.parent || null; } return null; } /** Get warning index */ function getWarningIndexes(warning) { const start = warning.start && sourceCode.getIndexFromLoc(warning.start); const end = warning.end && sourceCode.getIndexFromLoc(warning.end); return { start, end }; } } /** * Check if using TypeScript. */ function isUseTypeScript(context) { const sourceCode = context.sourceCode; if (sourceCode.parserServices.esTreeNodeToTSNodeMap) return true; const root = sourceCode.ast; for (const node of root.body) { if (node.type === 'SvelteScriptElement') { const lang = getLangValue(node)?.toLowerCase(); if (lang === 'ts' || lang === 'typescript') { return true; } } } return false; } /** * Check if using Babel. */ function isUseBabel(context) { const parser = context.parserOptions?.parser; if (!parser) { return false; } const sourceCode = context.sourceCode; const root = sourceCode.ast; let scriptLang = 'js'; for (const node of root.body) { if (node.type === 'SvelteScriptElement') { const lang = getLangValue(node)?.toLowerCase(); if (lang === 'ts' || lang === 'typescript') { scriptLang = lang; break; } } } const parserName = getParserName(scriptLang, parser); if (!parserName) { return false; } if (parserName === '@babel/eslint-parser') { return true; } if (parserName.includes('@babel/eslint-parser')) { let targetPath = parserName; while (targetPath) { const pkgPath = path.join(targetPath, 'package.json'); if (fs.existsSync(pkgPath)) { try { return JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))?.name === '@babel/eslint-parser'; } catch { return false; } } const parent = path.dirname(targetPath); if (targetPath === parent) { break; } targetPath = parent; } } return false; /** Get script parser name */ function getParserName(lang, parser) { if (typeof parser === 'string') { return parser; } else if (typeof parser === 'object') { const name = parser[lang]; if (typeof name === 'string') { return name; } } return null; } }