chore: better ignore code handling (#11606)

Instead of hacking an ignores array onto each node (and possibly degrading perf a bit because the object shape is mutated) we keep track of ignores in a stack. The new approach also avoids the indirection the old one had to do because the new approach looks upwards (checking if parent is a fragment) instead of iterating the children (checking for comments in them).
As a bonus unknown code warnings are now in order (line-column-wise) with the other warnings. Also fixes #11482 because text nodes of all shapes are ok
pull/11607/head
Simon H 8 months ago committed by GitHub
parent ac7709f65c
commit 2bc39b1de2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,4 +1,4 @@
import { filename, locator, warnings } from './state.js'; import { filename, locator, warnings, ignore_stack } from './state.js';
/** @typedef {{ start?: number, end?: number }} NodeLike */ /** @typedef {{ start?: number, end?: number }} NodeLike */
@ -8,8 +8,7 @@ import { filename, locator, warnings } from './state.js';
* @param {string} message * @param {string} message
*/ */
function w(node, code, message) { function w(node, code, message) {
// @ts-expect-error if (ignore_stack.at(-1)?.has(code)) return;
if (node?.ignores?.has(code)) return;
warnings.push({ warnings.push({
code, code,

@ -727,9 +727,6 @@ function check_element(node, state) {
for (const attribute of node.attributes) { for (const attribute of node.attributes) {
if (attribute.type !== 'Attribute') continue; if (attribute.type !== 'Attribute') continue;
// @ts-expect-error gross hack
attribute.ignores = node.ignores;
const name = attribute.name.toLowerCase(); const name = attribute.name.toLowerCase();
// aria-props // aria-props
if (name.startsWith('aria-')) { if (name.startsWith('aria-')) {

@ -25,6 +25,7 @@ import { prune } from './css/css-prune.js';
import { hash } from './utils.js'; import { hash } from './utils.js';
import { warn_unused } from './css/css-warn.js'; import { warn_unused } from './css/css-warn.js';
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js'; import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
import { pop_ignore, push_ignore } from '../../state.js';
/** /**
* @param {import('#compiler').Script | null} script * @param {import('#compiler').Script | null} script
@ -439,8 +440,7 @@ export function analyze_component(root, source, options) {
component_slots: new Set(), component_slots: new Set(),
expression: null, expression: null,
private_derived_state: [], private_derived_state: [],
function_depth: scope.function_depth, function_depth: scope.function_depth
ignores: new Set()
}; };
walk( walk(
@ -511,8 +511,7 @@ export function analyze_component(root, source, options) {
component_slots: new Set(), component_slots: new Set(),
expression: null, expression: null,
private_derived_state: [], private_derived_state: [],
function_depth: scope.function_depth, function_depth: scope.function_depth
ignores: new Set()
}; };
walk( walk(
@ -1100,67 +1099,51 @@ function is_safe_identifier(expression, scope) {
/** @type {import('./types').Visitors} */ /** @type {import('./types').Visitors} */
const common_visitors = { const common_visitors = {
_(node, context) { _(node, { state, next, path }) {
// @ts-expect-error const parent = path.at(-1);
const comments = /** @type {import('estree').Comment[]} */ (node.leadingComments); if (parent?.type === 'Fragment' && node.type !== 'Comment' && node.type !== 'Text') {
const idx = parent.nodes.indexOf(/** @type {any} */ (node));
if (comments) {
/** @type {string[]} */ /** @type {string[]} */
const ignores = []; const ignores = [];
for (let i = idx - 1; i >= 0; i--) {
for (const comment of comments) { const prev = parent.nodes[i];
const start = /** @type {any} */ (comment).start + 2; if (prev.type === 'Comment') {
ignores.push(...extract_svelte_ignore(start, comment.value, context.state.analysis.runes)); ignores.push(
} ...extract_svelte_ignore(
prev.start + 2 /* '//'.length */,
if (ignores.length > 0) { prev.data,
// @ts-expect-error see below state.analysis.runes
node.ignores = new Set([...context.state.ignores, ...ignores]); )
);
} else if (prev.type !== 'Text') {
break;
} }
} }
// @ts-expect-error if (ignores.length > 0) {
if (node.ignores) { push_ignore(ignores);
context.next({ next();
...context.state, pop_ignore();
// @ts-expect-error see below
ignores: node.ignores
});
} else if (context.state.ignores.size > 0) {
// @ts-expect-error
node.ignores = context.state.ignores;
} }
}, } else {
Fragment(node, context) { const comments = /** @type {any} */ (node).leadingComments;
if (comments) {
/** @type {string[]} */ /** @type {string[]} */
let ignores = []; const ignores = [];
for (const comment of comments) {
for (const child of node.nodes) { ignores.push(
if (child.type === 'Text' && child.data.trim() === '') { ...extract_svelte_ignore(
continue; comment.start + 4 /* '<!--'.length */,
comment.value,
state.analysis.runes
)
);
} }
if (ignores.length > 0) {
if (child.type === 'Comment') { push_ignore(ignores);
const start = next();
child.start + pop_ignore();
(context.state.analysis.source.slice(child.start, child.start + 4) === '<!--' ? 4 : 2);
ignores.push(...extract_svelte_ignore(start, child.data, context.state.analysis.runes));
} else {
const combined_ignores = new Set(context.state.ignores);
for (const ignore of ignores) combined_ignores.add(ignore);
if (combined_ignores.size > 0) {
// TODO this is a grotesque hack that's made necessary by the fact that
// we can't call `context.visit(...)` here, because we do the convoluted
// visitor merging thing. I'm increasingly of the view that we should
// rearchitect this stuff and have a single visitor per node. It'd be
// more efficient and much simpler.
// @ts-expect-error
child.ignores = combined_ignores;
} }
ignores = [];
} }
} }
}, },

@ -22,7 +22,6 @@ export interface AnalysisState {
expression: ExpressionTag | ClassDirective | SpreadAttribute | null; expression: ExpressionTag | ClassDirective | SpreadAttribute | null;
private_derived_state: string[]; private_derived_state: string[];
function_depth: number; function_depth: number;
ignores: Set<string>;
} }
export interface LegacyAnalysisState extends AnalysisState { export interface LegacyAnalysisState extends AnalysisState {

@ -10,6 +10,21 @@ export let filename;
export let locator = getLocator('', { offsetLine: 1 }); export let locator = getLocator('', { offsetLine: 1 });
/** @type {Set<string>[]} */
export let ignore_stack = [];
/**
* @param {string[]} ignores
*/
export function push_ignore(ignores) {
const next = new Set([...(ignore_stack.at(-1) || []), ...ignores]);
ignore_stack.push(next);
}
export function pop_ignore() {
ignore_stack.pop();
}
/** /**
* @param {{ * @param {{
* source: string; * source: string;
@ -20,4 +35,5 @@ export function reset(options) {
filename = options.filename; filename = options.filename;
locator = getLocator(options.source, { offsetLine: 1 }); locator = getLocator(options.source, { offsetLine: 1 });
warnings = []; warnings = [];
ignore_stack = [];
} }

@ -1,6 +1,6 @@
/* This file is generated by scripts/process-messages/index.js. Do not edit! */ /* This file is generated by scripts/process-messages/index.js. Do not edit! */
import { filename, locator, warnings } from './state.js'; import { filename, locator, warnings, ignore_stack } from './state.js';
/** @typedef {{ start?: number, end?: number }} NodeLike */ /** @typedef {{ start?: number, end?: number }} NodeLike */
/** /**
@ -9,8 +9,7 @@ import { filename, locator, warnings } from './state.js';
* @param {string} message * @param {string} message
*/ */
function w(node, code, message) { function w(node, code, message) {
// @ts-expect-error if (ignore_stack.at(-1)?.has(code)) return;
if (node?.ignores?.has(code)) return;
warnings.push({ warnings.push({
code, code,

@ -1,74 +1,74 @@
[ [
{ {
"code": "legacy_code", "code": "legacy_code",
"end": {
"column": 41,
"line": 3
},
"message": "`a11y-missing-attribute` is no longer valid — please use `a11y_missing_attribute` instead", "message": "`a11y-missing-attribute` is no longer valid — please use `a11y_missing_attribute` instead",
"start": { "start": {
"column": 19, "line": 3,
"line": 3 "column": 17
},
"end": {
"line": 3,
"column": 39
} }
}, },
{ {
"code": "unknown_code", "code": "a11y_missing_attribute",
"message": "`<img>` element should have an alt attribute",
"start": {
"line": 5,
"column": 1
},
"end": { "end": {
"column": 41, "line": 5,
"line": 8 "column": 29
}
}, },
{
"code": "unknown_code",
"message": "`ally_missing_attribute` is not a recognised code (did you mean `a11y_missing_attribute`?)", "message": "`ally_missing_attribute` is not a recognised code (did you mean `a11y_missing_attribute`?)",
"start": { "start": {
"column": 19, "line": 8,
"line": 8 "column": 17
}
}, },
{
"code": "legacy_code",
"end": { "end": {
"column": 39, "line": 8,
"line": 13 "column": 39
},
"message": "`a11y-misplaced-scope` is no longer valid — please use `a11y_misplaced_scope` instead",
"start": {
"column": 19,
"line": 13
} }
}, },
{ {
"code": "a11y_missing_attribute", "code": "a11y_missing_attribute",
"end": {
"column": 29,
"line": 5
},
"message": "`<img>` element should have an alt attribute", "message": "`<img>` element should have an alt attribute",
"start": { "start": {
"column": 1, "line": 10,
"line": 5 "column": 1
}
}, },
{
"code": "a11y_missing_attribute",
"end": { "end": {
"column": 29, "line": 10,
"line": 10 "column": 29
}
}, },
"message": "`<img>` element should have an alt attribute", {
"code": "legacy_code",
"message": "`a11y-misplaced-scope` is no longer valid — please use `a11y_misplaced_scope` instead",
"start": { "start": {
"column": 1, "line": 13,
"line": 10 "column": 17
},
"end": {
"line": 13,
"column": 37
} }
}, },
{ {
"code": "a11y_misplaced_scope", "code": "a11y_misplaced_scope",
"end": {
"column": 10,
"line": 14
},
"message": "The scope attribute should only be used with `<th>` elements", "message": "The scope attribute should only be used with `<th>` elements",
"start": { "start": {
"column": 5, "line": 14,
"line": 14 "column": 5
},
"end": {
"line": 14,
"column": 10
} }
} }
] ]

Loading…
Cancel
Save