chore: get more validator tests passing (#10714)

Get more validation tests passing:
- const tag cyclic validation (now runtime, based because of new reactivity system)
- illegal-variable-declaration
- illegal-attribute-character
- remove invalid-reactive-var validation as legacy reactive statements are transformed to functions under the hood, which never escape scope - arguably not completely correct, but will be what the user expects anyway
- invalid-rest-eachblock-binding
- remove edge-case redundant-event-modifier warning because event modifiers are deprecated anyway
- invalid-style-directive-modifier
- invalid-tag-property (now a different error)
- module-script-reactive-declaration
- take comment above script into account when silencing warnings
- invalid-css-declaration
- unused-export-let
- invalid-html-attribute
- illegal-store-subscription
- empty-block
pull/10718/head
Simon H 2 years ago committed by GitHub
parent 622195cc21
commit 881e84f988
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -110,7 +110,8 @@ const css = {
`:global(...) must not contain type or universal selectors when used in a compound selector`, `:global(...) must not contain type or universal selectors when used in a compound selector`,
'invalid-css-selector': () => `Invalid selector`, 'invalid-css-selector': () => `Invalid selector`,
'invalid-css-identifier': () => 'Expected a valid CSS identifier', 'invalid-css-identifier': () => 'Expected a valid CSS identifier',
'invalid-nesting-selector': () => `Nesting selectors can only be used inside a rule` 'invalid-nesting-selector': () => `Nesting selectors can only be used inside a rule`,
'invalid-css-declaration': () => 'Declaration cannot be empty'
}; };
/** @satisfies {Errors} */ /** @satisfies {Errors} */
@ -278,7 +279,9 @@ const attributes = {
directive2 directive2
)} directive`; )} directive`;
}, },
'invalid-let-directive-placement': () => 'let directive at invalid position' 'invalid-let-directive-placement': () => 'let directive at invalid position',
'invalid-style-directive-modifier': () =>
`Invalid 'style:' modifier. Valid modifiers are: 'important'`
}; };
/** @satisfies {Errors} */ /** @satisfies {Errors} */
@ -330,7 +333,11 @@ const variables = {
`${name} is an illegal variable name. To reference a global variable called ${name}, use globalThis.${name}`, `${name} is an illegal variable name. To reference a global variable called ${name}, use globalThis.${name}`,
/** @param {string} name */ /** @param {string} name */
'duplicate-declaration': (name) => `'${name}' has already been declared`, 'duplicate-declaration': (name) => `'${name}' has already been declared`,
'default-export': () => `A component cannot have a default export` 'default-export': () => `A component cannot have a default export`,
'illegal-variable-declaration': () =>
'Cannot declare same variable name which is imported inside <script context="module">',
'illegal-store-subscription': () =>
'Cannot subscribe to stores that are not declared at the top level of the component'
}; };
/** @satisfies {Errors} */ /** @satisfies {Errors} */
@ -435,11 +442,6 @@ const errors = {
// message // message
// }; // };
// }, // },
// contextual_store: {
// code: 'contextual-store',
// message:
// 'Stores must be declared at the top level of the component (this may change in a future version of Svelte)'
// },
// default_export: { // default_export: {
// code: 'default-export', // code: 'default-export',
// message: 'A component cannot have a default export' // message: 'A component cannot have a default export'
@ -448,31 +450,11 @@ const errors = {
// code: 'illegal-declaration', // code: 'illegal-declaration',
// message: 'The $ prefix is reserved, and cannot be used for variable and import names' // message: 'The $ prefix is reserved, and cannot be used for variable and import names'
// }, // },
// illegal_global: /** @param {string} name */ (name) => ({
// code: 'illegal-global',
// message: `${name} is an illegal variable name`
// }),
// illegal_variable_declaration: {
// code: 'illegal-variable-declaration',
// message: 'Cannot declare same variable name which is imported inside <script context="module">'
// },
// invalid_directive_value: { // invalid_directive_value: {
// code: 'invalid-directive-value', // code: 'invalid-directive-value',
// message: // message:
// 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)' // 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)'
// }, // },
// cyclical_const_tags: /** @param {string[]} cycle */ (cycle) => ({
// code: 'cyclical-const-tags',
// message: `Cyclical dependency detected: ${cycle.join(' → ')}`
// }),
// invalid_var_declaration: {
// code: 'invalid_var_declaration',
// message: '"var" scope should not extend outside the reactive block'
// },
// invalid_style_directive_modifier: /** @param {string} valid */ (valid) => ({
// code: 'invalid-style-directive-modifier',
// message: `Valid modifiers for style directives are: ${valid}`
// })
}; };
// interface is duplicated between here (used internally) and ./interfaces.js // interface is duplicated between here (used internally) and ./interfaces.js

@ -476,6 +476,10 @@ function read_declaration(parser) {
const value = read_value(parser); const value = read_value(parser);
if (!value && !property.startsWith('--')) {
error(parser.index, 'invalid-css-declaration');
}
const end = parser.index; const end = parser.index;
if (!parser.match('}')) { if (!parser.match('}')) {

@ -268,22 +268,41 @@ export default function tag(parser) {
if (name === 'script') { if (name === 'script') {
const content = read_script(parser, start, element.attributes); const content = read_script(parser, start, element.attributes);
if (content) { /** @type {import('#compiler').Comment | null} */
if (content.context === 'module') { let prev_comment = null;
if (current.module) error(start, 'duplicate-script-element'); for (let i = current.fragment.nodes.length - 1; i >= 0; i--) {
current.module = content; const node = current.fragment.nodes[i];
} else {
if (current.instance) error(start, 'duplicate-script-element'); if (i === current.fragment.nodes.length - 1 && node.end !== start) {
current.instance = content; break;
} }
if (node.type === 'Comment') {
prev_comment = node;
break;
} else if (node.type !== 'Text' || node.data.trim()) {
break;
}
}
if (prev_comment) {
// We take advantage of the fact that the root will never have leadingComments set,
// and set the previous comment to it so that the warning mechanism can later
// inspect the root and see if there was a html comment before it silencing specific warnings.
content.content.leadingComments = [{ type: 'Line', value: prev_comment.data }];
}
if (content.context === 'module') {
if (current.module) error(start, 'duplicate-script-element');
current.module = content;
} else {
if (current.instance) error(start, 'duplicate-script-element');
current.instance = content;
} }
} else { } else {
const content = read_style(parser, start, element.attributes); const content = read_style(parser, start, element.attributes);
if (content) { if (current.css) error(start, 'duplicate-style-element');
if (current.css) error(start, 'duplicate-style-element'); current.css = content;
current.css = content;
}
} }
return; return;
} }

@ -300,6 +300,25 @@ export function analyze_component(root, options) {
declaration.initial.source.value === 'svelte/store' declaration.initial.source.value === 'svelte/store'
)) ))
) { ) {
let is_nested_store_subscription = false;
for (const reference of references) {
for (let i = reference.path.length - 1; i >= 0; i--) {
const scope =
scopes.get(reference.path[i]) ||
module.scopes.get(reference.path[i]) ||
instance.scopes.get(reference.path[i]);
if (scope) {
const owner = scope?.owner(store_name);
is_nested_store_subscription =
!!owner && owner !== module.scope && owner !== instance.scope;
break;
}
}
}
if (is_nested_store_subscription) {
error(references[0].node, 'illegal-store-subscription');
}
if (options.runes !== false) { if (options.runes !== false) {
if (declaration === null && /[a-z]/.test(store_name[0])) { if (declaration === null && /[a-z]/.test(store_name[0])) {
error(references[0].node, 'illegal-global', name); error(references[0].node, 'illegal-global', name);
@ -442,6 +461,17 @@ export function analyze_component(root, options) {
); );
} }
for (const [name, binding] of instance.scope.declarations) {
if (binding.kind === 'prop' && binding.node.name !== '$$props') {
const references = binding.references.filter(
(r) => r.node !== binding.node && r.path.at(-1)?.type !== 'ExportSpecifier'
);
if (!references.length && !instance.scope.declarations.has(`$${name}`)) {
warn(warnings, binding.node, [], 'unused-export-let', name);
}
}
}
analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements); analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements);
} }
@ -590,6 +620,17 @@ const legacy_scope_tweaker = {
state.reactive_statements.set(node, reactive_statement); state.reactive_statements.set(node, reactive_statement);
// Ideally this would be in the validation file, but that isn't possible because this visitor
// calls "next" before setting the reactive statements.
if (
reactive_statement.dependencies.size &&
[...reactive_statement.dependencies].every(
(d) => d.scope === state.analysis.module.scope && d.declaration_kind !== 'const'
)
) {
warn(state.analysis.warnings, node, path, 'module-script-reactive-declaration');
}
if ( if (
node.body.type === 'ExpressionStatement' && node.body.type === 'ExpressionStatement' &&
node.body.expression.type === 'AssignmentExpression' node.body.expression.type === 'AssignmentExpression'
@ -710,7 +751,8 @@ const legacy_scope_tweaker = {
if ( if (
binding.kind === 'state' || binding.kind === 'state' ||
binding.kind === 'frozen_state' || binding.kind === 'frozen_state' ||
(binding.kind === 'normal' && binding.declaration_kind === 'let') (binding.kind === 'normal' &&
(binding.declaration_kind === 'let' || binding.declaration_kind === 'var'))
) { ) {
binding.kind = 'prop'; binding.kind = 'prop';
if (specifier.exported.name !== specifier.local.name) { if (specifier.exported.name !== specifier.local.name) {

@ -49,8 +49,12 @@ function validate_component(node, context) {
error(attribute, 'invalid-event-modifier'); error(attribute, 'invalid-event-modifier');
} }
if (attribute.type === 'Attribute' && attribute.name === 'slot') { if (attribute.type === 'Attribute') {
validate_slot_attribute(context, attribute); validate_attribute_name(attribute, context);
if (attribute.name === 'slot') {
validate_slot_attribute(context, attribute);
}
} }
} }
@ -61,6 +65,11 @@ function validate_component(node, context) {
}); });
} }
const react_attributes = new Map([
['className', 'class'],
['htmlFor', 'for']
]);
/** /**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context * @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context
@ -105,6 +114,20 @@ function validate_element(node, context) {
if (attribute.name === 'is' && context.state.options.namespace !== 'foreign') { if (attribute.name === 'is' && context.state.options.namespace !== 'foreign') {
warn(context.state.analysis.warnings, attribute, context.path, 'avoid-is'); warn(context.state.analysis.warnings, attribute, context.path, 'avoid-is');
} }
const correct_name = react_attributes.get(attribute.name);
if (correct_name) {
warn(
context.state.analysis.warnings,
attribute,
context.path,
'invalid-html-attribute',
attribute.name,
correct_name
);
}
validate_attribute_name(attribute, context);
} else if (attribute.type === 'AnimateDirective') { } else if (attribute.type === 'AnimateDirective') {
const parent = context.path.at(-2); const parent = context.path.at(-2);
if (parent?.type !== 'EachBlock') { if (parent?.type !== 'EachBlock') {
@ -166,6 +189,21 @@ function validate_element(node, context) {
} }
} }
/**
* @param {import('#compiler').Attribute} attribute
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context
*/
function validate_attribute_name(attribute, context) {
if (
attribute.name.includes(':') &&
!attribute.name.startsWith('xmlns:') &&
!attribute.name.startsWith('xlink:') &&
!attribute.name.startsWith('xml:')
) {
warn(context.state.analysis.warnings, attribute, context.path, 'illegal-attribute-character');
}
}
/** /**
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context * @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context
* @param {import('#compiler').Attribute} attribute * @param {import('#compiler').Attribute} attribute
@ -231,6 +269,19 @@ function validate_slot_attribute(context, attribute) {
} }
} }
/**
* @param {import('#compiler').Fragment | null | undefined} node
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context
*/
function validate_block_not_empty(node, context) {
if (!node) return;
// Assumption: If the block has zero elements, someone's in the middle of typing it out,
// so don't warn in that case because it would be distracting.
if (node.nodes.length === 1 && node.nodes[0].type === 'Text' && !node.nodes[0].raw.trim()) {
warn(context.state.analysis.warnings, node.nodes[0], context.path, 'empty-block');
}
}
/** /**
* @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} * @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, import('./types.js').AnalysisState>}
*/ */
@ -277,13 +328,24 @@ const validation = {
// TODO handle mutations of non-state/props in runes mode // TODO handle mutations of non-state/props in runes mode
} }
const binding = context.state.scope.get(left.name);
if (node.name === 'group') { if (node.name === 'group') {
const binding = context.state.scope.get(left.name);
if (!binding) { if (!binding) {
error(node, 'INTERNAL', 'Cannot find declaration for bind:group'); error(node, 'INTERNAL', 'Cannot find declaration for bind:group');
} }
} }
if (binding?.kind === 'each' && binding.metadata?.inside_rest) {
warn(
context.state.analysis.warnings,
binding.node,
context.path,
'invalid-rest-eachblock-binding',
binding.node.name
);
}
const parent = context.path.at(-1); const parent = context.path.at(-1);
if ( if (
@ -521,8 +583,28 @@ const validation = {
); );
} }
}, },
SnippetBlock(node, { path }) { IfBlock(node, context) {
validate_block_not_empty(node.consequent, context);
validate_block_not_empty(node.alternate, context);
},
EachBlock(node, context) {
validate_block_not_empty(node.body, context);
validate_block_not_empty(node.fallback, context);
},
AwaitBlock(node, context) {
validate_block_not_empty(node.pending, context);
validate_block_not_empty(node.then, context);
validate_block_not_empty(node.catch, context);
},
KeyBlock(node, context) {
validate_block_not_empty(node.fragment, context);
},
SnippetBlock(node, context) {
validate_block_not_empty(node.body, context);
if (node.expression.name !== 'children') return; if (node.expression.name !== 'children') return;
const { path } = context;
const parent = path.at(-2); const parent = path.at(-2);
if (!parent) return; if (!parent) return;
if ( if (
@ -539,6 +621,11 @@ const validation = {
} }
} }
}, },
StyleDirective(node) {
if (node.modifiers.length > 1 || (node.modifiers.length && node.modifiers[0] !== 'important')) {
error(node, 'invalid-style-directive-modifier');
}
},
SvelteHead(node) { SvelteHead(node) {
const attribute = node.attributes[0]; const attribute = node.attributes[0];
if (attribute) { if (attribute) {
@ -622,6 +709,8 @@ const validation = {
export const validation_legacy = merge(validation, a11y_validators, { export const validation_legacy = merge(validation, a11y_validators, {
VariableDeclarator(node, { state }) { VariableDeclarator(node, { state }) {
ensure_no_module_import_conflict(node, state);
if (node.init?.type !== 'CallExpression') return; if (node.init?.type !== 'CallExpression') return;
const callee = node.init.callee; const callee = node.init.callee;
@ -732,6 +821,22 @@ function validate_call_expression(node, scope, path) {
} }
} }
/**
* @param {import('estree').VariableDeclarator} node
* @param {import('./types.js').AnalysisState} state
*/
function ensure_no_module_import_conflict(node, state) {
const ids = extract_identifiers(node.id);
for (const id of ids) {
if (
state.scope === state.analysis.instance.scope &&
state.analysis.module.scope.get(id.name)?.declaration_kind === 'import'
) {
error(node.id, 'illegal-variable-declaration');
}
}
}
/** /**
* @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} * @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, import('./types.js').AnalysisState>}
*/ */
@ -912,6 +1017,8 @@ export const validation_runes = merge(validation, a11y_validators, {
next({ ...state }); next({ ...state });
}, },
VariableDeclarator(node, { state, path }) { VariableDeclarator(node, { state, path }) {
ensure_no_module_import_conflict(node, state);
const init = node.init; const init = node.init;
const rune = get_rune(init, state.scope); const rune = get_rune(init, state.scope);

@ -1800,6 +1800,12 @@ export const template_visitors = {
) )
) )
); );
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (state.options.dev) {
state.init.push(b.stmt(b.call('$.get', declaration.id)));
}
} else { } else {
const identifiers = extract_identifiers(declaration.id); const identifiers = extract_identifiers(declaration.id);
const tmp = b.id(state.scope.generate('computed_const')); const tmp = b.id(state.scope.generate('computed_const'));
@ -1829,6 +1835,12 @@ export const template_visitors = {
b.const(tmp, b.call(state.options.runes ? '$.derived' : '$.derived_safe_equal', fn)) b.const(tmp, b.call(state.options.runes ? '$.derived' : '$.derived_safe_equal', fn))
); );
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (state.options.dev) {
state.init.push(b.stmt(b.call('$.get', tmp)));
}
for (const node of identifiers) { for (const node of identifiers) {
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(node.name)); const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(node.name));
binding.expression = b.member(b.call('$.get', tmp), node); binding.expression = b.member(b.call('$.get', tmp), node);

@ -14,7 +14,7 @@ export class Scope {
* The immediate parent scope * The immediate parent scope
* @type {Scope | null} * @type {Scope | null}
*/ */
#parent; parent;
/** /**
* Whether or not `var` declarations are contained by this scope * Whether or not `var` declarations are contained by this scope
@ -56,7 +56,7 @@ export class Scope {
*/ */
constructor(root, parent, porous) { constructor(root, parent, porous) {
this.root = root; this.root = root;
this.#parent = parent; this.parent = parent;
this.#porous = porous; this.#porous = porous;
this.function_depth = parent ? parent.function_depth + (porous ? 0 : 1) : 0; this.function_depth = parent ? parent.function_depth + (porous ? 0 : 1) : 0;
} }
@ -83,13 +83,13 @@ export class Scope {
error(node, 'invalid-dollar-prefix'); error(node, 'invalid-dollar-prefix');
} }
if (this.#parent) { if (this.parent) {
if (declaration_kind === 'var' && this.#porous) { if (declaration_kind === 'var' && this.#porous) {
return this.#parent.declare(node, kind, declaration_kind); return this.parent.declare(node, kind, declaration_kind);
} }
if (declaration_kind === 'import') { if (declaration_kind === 'import') {
return this.#parent.declare(node, kind, declaration_kind, initial); return this.parent.declare(node, kind, declaration_kind, initial);
} }
} }
@ -112,7 +112,8 @@ export class Scope {
prop_alias: null, prop_alias: null,
expression: null, expression: null,
mutation: null, mutation: null,
reassigned: false reassigned: false,
metadata: null
}; };
this.declarations.set(node.name, binding); this.declarations.set(node.name, binding);
this.root.conflicts.add(node.name); this.root.conflicts.add(node.name);
@ -129,7 +130,7 @@ export class Scope {
*/ */
generate(preferred_name) { generate(preferred_name) {
if (this.#porous) { if (this.#porous) {
return /** @type {Scope} */ (this.#parent).generate(preferred_name); return /** @type {Scope} */ (this.parent).generate(preferred_name);
} }
preferred_name = preferred_name.replace(/[^a-zA-Z0-9_$]/g, '_').replace(/^[0-9]/, '_'); preferred_name = preferred_name.replace(/[^a-zA-Z0-9_$]/g, '_').replace(/^[0-9]/, '_');
@ -155,7 +156,7 @@ export class Scope {
* @returns {import('#compiler').Binding | null} * @returns {import('#compiler').Binding | null}
*/ */
get(name) { get(name) {
return this.declarations.get(name) ?? this.#parent?.get(name) ?? null; return this.declarations.get(name) ?? this.parent?.get(name) ?? null;
} }
/** /**
@ -175,7 +176,7 @@ export class Scope {
* @returns {Scope | null} * @returns {Scope | null}
*/ */
owner(name) { owner(name) {
return this.declarations.has(name) ? this : this.#parent && this.#parent.owner(name); return this.declarations.has(name) ? this : this.parent && this.parent.owner(name);
} }
/** /**
@ -193,8 +194,8 @@ export class Scope {
const binding = this.declarations.get(node.name); const binding = this.declarations.get(node.name);
if (binding) { if (binding) {
binding.references.push({ node, path }); binding.references.push({ node, path });
} else if (this.#parent) { } else if (this.parent) {
this.#parent.reference(node, path); this.parent.reference(node, path);
} else { } else {
// no binding was found, and this is the top level scope, // no binding was found, and this is the top level scope,
// which means this is a global // which means this is a global
@ -534,11 +535,31 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
// declarations // declarations
for (const id of extract_identifiers(node.context)) { for (const id of extract_identifiers(node.context)) {
scope.declare(id, 'each', 'const'); const binding = scope.declare(id, 'each', 'const');
let inside_rest = false;
let is_rest_id = false;
walk(node.context, null, {
Identifier(node) {
if (inside_rest && node === id) {
is_rest_id = true;
}
},
RestElement(_, { next }) {
const prev = inside_rest;
inside_rest = true;
next();
inside_rest = prev;
}
});
binding.metadata = { inside_rest: is_rest_id };
} }
if (node.context.type !== 'Identifier') { if (node.context.type !== 'Identifier') {
scope.declare(b.id('$$item'), 'derived', 'synthetic'); scope.declare(b.id('$$item'), 'derived', 'synthetic');
} }
// Visit to pick up references from default initializers
visit(node.context, { scope });
if (node.index) { if (node.index) {
const is_keyed = const is_keyed =

@ -288,6 +288,11 @@ export interface Binding {
expression: Expression | ((id: Identifier) => Expression) | null; expression: Expression | ((id: Identifier) => Expression) | null;
/** If this is set, all mutations should use this expression */ /** If this is set, all mutations should use this expression */
mutation: ((assignment: AssignmentExpression, context: Context<any, any>) => Expression) | null; mutation: ((assignment: AssignmentExpression, context: Context<any, any>) => Expression) | null;
/** Additional metadata, varies per binding type */
metadata: {
/** `true` if is (inside) a rest parameter */
inside_rest?: boolean;
} | null;
} }
export * from './template.js'; export * from './template.js';

@ -13,12 +13,9 @@ import type {
ObjectExpression, ObjectExpression,
Pattern, Pattern,
Program, Program,
SpreadElement,
CallExpression,
ChainExpression, ChainExpression,
SimpleCallExpression SimpleCallExpression
} from 'estree'; } from 'estree';
import type { Atrule, Rule } from './css';
export interface BaseNode { export interface BaseNode {
type: string; type: string;

@ -15,7 +15,15 @@ const attributes = {
'avoid-is': () => 'The "is" attribute is not supported cross-browser and should be avoided', 'avoid-is': () => 'The "is" attribute is not supported cross-browser and should be avoided',
/** @param {string} name */ /** @param {string} name */
'global-event-reference': (name) => 'global-event-reference': (name) =>
`You are referencing globalThis.${name}. Did you forget to declare a variable with that name?` `You are referencing globalThis.${name}. Did you forget to declare a variable with that name?`,
'illegal-attribute-character': () =>
"Attributes should not contain ':' characters to prevent ambiguity with Svelte directives",
/**
* @param {string} wrong
* @param {string} right
*/
'invalid-html-attribute': (wrong, right) =>
`'${wrong}' is not a valid HTML attribute. Did you mean '${right}'?`
}; };
/** @satisfies {Warnings} */ /** @satisfies {Warnings} */
@ -195,7 +203,10 @@ const a11y = {
/** @satisfies {Warnings} */ /** @satisfies {Warnings} */
const state = { const state = {
'static-state-reference': () => 'static-state-reference': () =>
`State referenced in its own scope will never update. Did you mean to reference it inside a closure?` `State referenced in its own scope will never update. Did you mean to reference it inside a closure?`,
/** @param {string} name */
'invalid-rest-eachblock-binding': (name) =>
`The rest operator (...) will create a new object and binding '${name}' with the original object will not work`
}; };
/** @satisfies {Warnings} */ /** @satisfies {Warnings} */
@ -214,7 +225,16 @@ const components = {
const legacy = { const legacy = {
'no-reactive-declaration': () => 'no-reactive-declaration': () =>
`Reactive declarations only exist at the top level of the instance script` `Reactive declarations only exist at the top level of the instance script`,
'module-script-reactive-declaration': () =>
'All dependencies of the reactive declaration are declared in a module script and will not be reactive',
/** @param {string} name */
'unused-export-let': (name) =>
`Component has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\``
};
const block = {
'empty-block': () => 'Empty block'
}; };
/** @satisfies {Warnings} */ /** @satisfies {Warnings} */
@ -226,7 +246,8 @@ const warnings = {
...performance, ...performance,
...state, ...state,
...components, ...components,
...legacy ...legacy,
...block
}; };
/** @typedef {typeof warnings} AllWarnings */ /** @typedef {typeof warnings} AllWarnings */
@ -234,7 +255,7 @@ const warnings = {
/** /**
* @template {keyof AllWarnings} T * @template {keyof AllWarnings} T
* @param {import('./phases/types').RawWarning[]} array the array to push the warning to, if not ignored * @param {import('./phases/types').RawWarning[]} array the array to push the warning to, if not ignored
* @param {{ start?: number, end?: number, parent?: import('#compiler').SvelteNode | null, leadingComments?: import('estree').Comment[] } | null} node the node related to the warning * @param {{ start?: number, end?: number, type?: string, parent?: import('#compiler').SvelteNode | null, leadingComments?: import('estree').Comment[] } | null} node the node related to the warning
* @param {import('#compiler').SvelteNode[]} path the path to the node, so that we can traverse upwards to find svelte-ignore comments * @param {import('#compiler').SvelteNode[]} path the path to the node, so that we can traverse upwards to find svelte-ignore comments
* @param {T} code the warning code * @param {T} code the warning code
* @param {Parameters<AllWarnings[T]>} args the arguments to pass to the warning function * @param {Parameters<AllWarnings[T]>} args the arguments to pass to the warning function
@ -249,6 +270,13 @@ export function warn(array, node, path, code, ...args) {
/** @type {string[]} */ /** @type {string[]} */
const ignores = []; const ignores = [];
if (node) {
// comments inside JavaScript (estree)
if ('leadingComments' in node) {
ignores.push(...extract_svelte_ignore_from_comments(node));
}
}
for (let i = path.length - 1; i >= 0; i--) { for (let i = path.length - 1; i >= 0; i--) {
const current = path[i]; const current = path[i];

@ -1,10 +1,8 @@
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
skip: true,
error: { error: {
code: '', code: 'illegal-store-subscription',
message: message: 'Cannot subscribe to stores that are not declared at the top level of the component'
'Stores must be declared at the top level of the component (this may change in a future version of Svelte)'
} }
}); });

@ -1,9 +1,8 @@
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
skip: true,
error: { error: {
code: '', code: 'invalid-dollar-prefix',
message: 'The $ prefix is reserved, and cannot be used for variable and import names' message: 'The $ prefix is reserved, and cannot be used for variables and imports'
} }
}); });

@ -1,10 +1,8 @@
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
skip: true,
error: { error: {
code: '', code: 'illegal-store-subscription',
message: message: 'Cannot subscribe to stores that are not declared at the top level of the component'
'Stores must be declared at the top level of the component (this may change in a future version of Svelte)'
} }
}); });

@ -1,10 +1,8 @@
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
skip: true,
error: { error: {
code: '', code: 'illegal-store-subscription',
message: message: 'Cannot subscribe to stores that are not declared at the top level of the component'
'Stores must be declared at the top level of the component (this may change in a future version of Svelte)'
} }
}); });

@ -44,6 +44,7 @@
"column": 0 "column": 0
} }
}, },
"leadingComments": [{ "type": "Line", "value": "should not error out" }],
"body": [ "body": [
{ {
"type": "VariableDeclaration", "type": "VariableDeclaration",

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
error: "Cannot access 'c' before initialization"
});

@ -1,8 +1,8 @@
<script> <script>
export let array; export let array = [1];
</script> </script>
{#each array as a} {#each array as a}
{@const b = a + c} {@const b = a + c}
{@const c = b + a} {@const c = b + a}
{/each} {/each}

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,8 +0,0 @@
[
{
"code": "cyclical-const-tags",
"message": "Cyclical dependency detected: b → c → b",
"start": { "line": 6, "column": 2 },
"end": { "line": 6, "column": 20 }
}
]

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -2,8 +2,17 @@
let things = []; let things = [];
</script> </script>
<!-- ok (zero elements very likely means someone's in the middle of typing) -->
{#each things as thing}{/each}
{#if true}{/if}
{#key things}x{/key}
{#await promise}{things}
{/await}
<!-- invalid -->
{#each things as thing} {#each things as thing}
{/each} {/each}
{#if true} {/if}
{#each things as thing}{/each} {#key things} {/key}
{#await promise} {/await}

@ -2,25 +2,25 @@
{ {
"code": "empty-block", "code": "empty-block",
"message": "Empty block", "message": "Empty block",
"start": { "start": { "line": 13, "column": 23 },
"line": 5, "end": { "line": 15, "column": 0 }
"column": 0
},
"end": {
"line": 7,
"column": 7
}
}, },
{ {
"code": "empty-block", "code": "empty-block",
"message": "Empty block", "message": "Empty block",
"start": { "start": { "line": 16, "column": 10 },
"line": 9, "end": { "line": 16, "column": 11 }
"column": 0 },
}, {
"end": { "code": "empty-block",
"line": 9, "message": "Empty block",
"column": 30 "start": { "line": 17, "column": 13 },
} "end": { "line": 17, "column": 14 }
},
{
"code": "empty-block",
"message": "Empty block",
"start": { "line": 18, "column": 16 },
"end": { "line": 18, "column": 17 }
} }
] ]

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,12 +0,0 @@
<script>
function handleTouchstart() {
// ...
}
function handleClick() {
// ...
}
</script>
<button on:click|passive="{handleClick}"></button>
<div on:touchstart|passive="{handleTouchstart}"></div>

@ -1,26 +0,0 @@
[
{
"message": "The passive modifier only works with wheel and touch events",
"code": "redundant-event-modifier",
"start": {
"line": 11,
"column": 8
},
"end": {
"line": 11,
"column": 40
}
},
{
"message": "Touch event handlers that don't use the 'event' object are passive by default",
"code": "redundant-event-modifier",
"start": {
"line": 12,
"column": 5
},
"end": {
"line": 12,
"column": 47
}
}
]

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -3,12 +3,12 @@
"code": "illegal-variable-declaration", "code": "illegal-variable-declaration",
"message": "Cannot declare same variable name which is imported inside <script context=\"module\">", "message": "Cannot declare same variable name which is imported inside <script context=\"module\">",
"start": { "start": {
"line": 6, "line": 12,
"column": 1 "column": 5
}, },
"end": { "end": {
"line": 6, "line": 12,
"column": 9 "column": 8
} }
} }
] ]

@ -1,8 +1,14 @@
<script context="module"> <script context="module">
import { FOO } from './dummy.svelte'; import { FOO } from './dummy.svelte';
function ok() {
let FOO;
}
</script> </script>
<script> <script>
function ok() {
let FOO;
}
let FOO; let FOO;
</script> </script>

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,14 +1,14 @@
[ [
{ {
"code": "invalid-declaration", "code": "invalid-css-declaration",
"message": "Declaration cannot be empty", "message": "Declaration cannot be empty",
"start": { "start": {
"line": 11, "line": 5,
"column": 0 "column": 8
}, },
"end": { "end": {
"line": 11, "line": 5,
"column": 0 "column": 8
} }
} }
] ]

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,8 +0,0 @@
[
{
"code": "invalid_var_declaration",
"message": "\"var\" scope should not extend outside the reactive block",
"start": { "line": 14, "column": 7 },
"end": { "line": 14, "column": 16 }
}
]

@ -1,20 +0,0 @@
<script>
var a;
var {a, b: [d, f], c}= {a: 1, b: [1, 2], c: 2};
$: {
function one() {
var a = 'a';
function two() {
var a = 'b';
return a;
}
return two();
}
a = one();
for (var i = 0; i<5; i ++ ) {
// Todo
}
}
</script>
<h1>Hello {a}</h1>

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,8 +0,0 @@
[
{
"code": "invalid_var_declaration",
"message": "\"var\" scope should not extend outside the reactive block",
"start": { "line": 4, "column": 2 },
"end": { "line": 4, "column": 50 }
}
]

@ -1,8 +0,0 @@
<script>
$: {
let f = 'f';
var {a, b: [c, d], e} = {a: 1, b: [2, 3], e: 4};
}
</script>
<h1>Hello</h1>

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,9 +1,9 @@
[ [
{ {
"code": "module-script-reactive-declaration", "code": "module-script-reactive-declaration",
"message": "\"foo\" is declared in a module script and will not be reactive", "message": "All dependencies of the reactive declaration are declared in a module script and will not be reactive",
"start": { "start": {
"column": 4, "column": 1,
"line": 5 "line": 5
}, },
"end": { "end": {

@ -1,4 +0,0 @@
import { test } from '../../test';
// TODO this likely works in the new world - remove this warning?
export default test({ skip: true });

@ -1,4 +0,0 @@
import { test } from '../../test';
// TODO this likely works in the new world - remove this warning?
export default test({ skip: true });

@ -1,4 +0,0 @@
import { test } from '../../test';
// TODO this maybe works in the new world - remove this warning?
export default test({ skip: true });

@ -1,4 +0,0 @@
import { test } from '../../test';
// TODO this likely works in the new world - remove this warning?
export default test({ skip: true });

@ -2,8 +2,7 @@
let foo; let foo;
</script> </script>
<!-- svelte-ignore unused-export-let module-script-reactive-declaration --> <!-- svelte-ignore module-script-reactive-declaration -->
<script> <script>
export let unused;
$: reactive = foo; $: reactive = foo;
</script> </script>

@ -3,8 +3,6 @@
</script> </script>
<script> <script>
// svelte-ignore unused-export-let
export let unused;
// svelte-ignore module-script-reactive-declaration // svelte-ignore module-script-reactive-declaration
$: reactive = foo; $: reactive = foo;
</script> </script>

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,6 +1,6 @@
[ [
{ {
"message": "Valid modifiers for style directives are: important", "message": "Invalid 'style:' modifier. Valid modifiers are: 'important'",
"code": "invalid-style-directive-modifier", "code": "invalid-style-directive-modifier",
"start": { "start": {
"line": 1, "line": 1,

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,7 +1,7 @@
[ [
{ {
"code": "invalid-tag-property", "code": "invalid-tag-property",
"message": "tag name must be two or more words joined by the '-' character", "message": "tag name must be two or more words joined by the \"-\" character",
"start": { "start": {
"line": 1, "line": 1,
"column": 16 "column": 16

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,14 +1,14 @@
[ [
{ {
"code": "invalid-tag-attribute", "code": "invalid-svelte-option-customElement",
"message": "'tag' must be a string literal", "message": "\"customElement\" must be a string literal defining a valid custom element name or an object of the form { tag: string; shadow?: \"open\" | \"none\"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }",
"start": { "start": {
"line": 1, "line": 1,
"column": 16 "column": 16
}, },
"end": { "end": {
"line": 1, "line": 1,
"column": 24 "column": 34
} }
} }
] ]

@ -1 +1 @@
<svelte:options tag={42}/> <svelte:options customElement={42}/>

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,7 +1,7 @@
[ [
{ {
"code": "unused-export-let", "code": "unused-export-let",
"message": "Component_1 has unused export property 'default_value_5'. If it is for external reference only, please consider using `export const default_value_5`", "message": "Component has unused export property 'default_value_5'. If it is for external reference only, please consider using `export const default_value_5`",
"start": { "start": {
"column": 12, "column": 12,
"line": 8 "line": 8
@ -13,7 +13,7 @@
}, },
{ {
"code": "unused-export-let", "code": "unused-export-let",
"message": "Component_1 has unused export property 'default_value_6'. If it is for external reference only, please consider using `export const default_value_6`", "message": "Component has unused export property 'default_value_6'. If it is for external reference only, please consider using `export const default_value_6`",
"start": { "start": {
"column": 12, "column": 12,
"line": 9 "line": 9

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -2,31 +2,31 @@
{ {
"code": "unused-export-let", "code": "unused-export-let",
"end": { "end": {
"column": 13, "column": 8,
"line": 31 "line": 28
}, },
"message": "Component has unused export property 'd2'. If it is for external reference only, please consider using `export const d2`", "message": "Component has unused export property 'd2'. If it is for external reference only, please consider using `export const d2`",
"start": { "start": {
"column": 11, "column": 6,
"line": 31 "line": 28
} }
}, },
{ {
"code": "unused-export-let", "code": "unused-export-let",
"end": { "end": {
"column": 17, "column": 8,
"line": 31 "line": 29
}, },
"message": "Component has unused export property 'e2'. If it is for external reference only, please consider using `export const e2`", "message": "Component has unused export property 'e2'. If it is for external reference only, please consider using `export const e2`",
"start": { "start": {
"column": 15, "column": 6,
"line": 31 "line": 29
} }
}, },
{ {
"code": "unused-export-let", "code": "unused-export-let",
"end": { "end": {
"column": 19, "column": 15,
"line": 32 "line": 32
}, },
"message": "Component has unused export property 'g2'. If it is for external reference only, please consider using `export const g2`", "message": "Component has unused export property 'g2'. If it is for external reference only, please consider using `export const g2`",
@ -38,7 +38,7 @@
{ {
"code": "unused-export-let", "code": "unused-export-let",
"end": { "end": {
"column": 19, "column": 15,
"line": 33 "line": 33
}, },
"message": "Component has unused export property 'h2'. If it is for external reference only, please consider using `export const h2`", "message": "Component has unused export property 'h2'. If it is for external reference only, please consider using `export const h2`",
@ -50,7 +50,7 @@
{ {
"code": "unused-export-let", "code": "unused-export-let",
"end": { "end": {
"column": 26, "column": 15,
"line": 35 "line": 35
}, },
"message": "Component has unused export property 'j2'. If it is for external reference only, please consider using `export const j2`", "message": "Component has unused export property 'j2'. If it is for external reference only, please consider using `export const j2`",

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -756,6 +756,11 @@ declare module 'svelte/compiler' {
expression: Expression | ((id: Identifier) => Expression) | null; expression: Expression | ((id: Identifier) => Expression) | null;
/** If this is set, all mutations should use this expression */ /** If this is set, all mutations should use this expression */
mutation: ((assignment: AssignmentExpression, context: Context<any, any>) => Expression) | null; mutation: ((assignment: AssignmentExpression, context: Context<any, any>) => Expression) | null;
/** Additional metadata, varies per binding type */
metadata: {
/** `true` if is (inside) a rest parameter */
inside_rest?: boolean;
} | null;
} }
interface BaseNode_1 { interface BaseNode_1 {
type: string; type: string;
@ -1020,6 +1025,10 @@ declare module 'svelte/compiler' {
constructor(root: ScopeRoot, parent: Scope | null, porous: boolean); constructor(root: ScopeRoot, parent: Scope | null, porous: boolean);
root: ScopeRoot; root: ScopeRoot;
/**
* The immediate parent scope
* */
parent: Scope | null;
/** /**
* A map of every identifier declared by this scope, and all the * A map of every identifier declared by this scope, and all the
* identifiers that reference it * identifiers that reference it

Loading…
Cancel
Save