Skip to content

Commit 881e84f

Browse files
authored
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
1 parent 622195c commit 881e84f

File tree

58 files changed

+374
-284
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+374
-284
lines changed

packages/svelte/src/compiler/errors.js

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ const css = {
110110
`:global(...) must not contain type or universal selectors when used in a compound selector`,
111111
'invalid-css-selector': () => `Invalid selector`,
112112
'invalid-css-identifier': () => 'Expected a valid CSS identifier',
113-
'invalid-nesting-selector': () => `Nesting selectors can only be used inside a rule`
113+
'invalid-nesting-selector': () => `Nesting selectors can only be used inside a rule`,
114+
'invalid-css-declaration': () => 'Declaration cannot be empty'
114115
};
115116

116117
/** @satisfies {Errors} */
@@ -278,7 +279,9 @@ const attributes = {
278279
directive2
279280
)} directive`;
280281
},
281-
'invalid-let-directive-placement': () => 'let directive at invalid position'
282+
'invalid-let-directive-placement': () => 'let directive at invalid position',
283+
'invalid-style-directive-modifier': () =>
284+
`Invalid 'style:' modifier. Valid modifiers are: 'important'`
282285
};
283286

284287
/** @satisfies {Errors} */
@@ -330,7 +333,11 @@ const variables = {
330333
`${name} is an illegal variable name. To reference a global variable called ${name}, use globalThis.${name}`,
331334
/** @param {string} name */
332335
'duplicate-declaration': (name) => `'${name}' has already been declared`,
333-
'default-export': () => `A component cannot have a default export`
336+
'default-export': () => `A component cannot have a default export`,
337+
'illegal-variable-declaration': () =>
338+
'Cannot declare same variable name which is imported inside <script context="module">',
339+
'illegal-store-subscription': () =>
340+
'Cannot subscribe to stores that are not declared at the top level of the component'
334341
};
335342

336343
/** @satisfies {Errors} */
@@ -435,11 +442,6 @@ const errors = {
435442
// message
436443
// };
437444
// },
438-
// contextual_store: {
439-
// code: 'contextual-store',
440-
// message:
441-
// 'Stores must be declared at the top level of the component (this may change in a future version of Svelte)'
442-
// },
443445
// default_export: {
444446
// code: 'default-export',
445447
// message: 'A component cannot have a default export'
@@ -448,31 +450,11 @@ const errors = {
448450
// code: 'illegal-declaration',
449451
// message: 'The $ prefix is reserved, and cannot be used for variable and import names'
450452
// },
451-
// illegal_global: /** @param {string} name */ (name) => ({
452-
// code: 'illegal-global',
453-
// message: `${name} is an illegal variable name`
454-
// }),
455-
// illegal_variable_declaration: {
456-
// code: 'illegal-variable-declaration',
457-
// message: 'Cannot declare same variable name which is imported inside <script context="module">'
458-
// },
459453
// invalid_directive_value: {
460454
// code: 'invalid-directive-value',
461455
// message:
462456
// 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)'
463457
// },
464-
// cyclical_const_tags: /** @param {string[]} cycle */ (cycle) => ({
465-
// code: 'cyclical-const-tags',
466-
// message: `Cyclical dependency detected: ${cycle.join(' → ')}`
467-
// }),
468-
// invalid_var_declaration: {
469-
// code: 'invalid_var_declaration',
470-
// message: '"var" scope should not extend outside the reactive block'
471-
// },
472-
// invalid_style_directive_modifier: /** @param {string} valid */ (valid) => ({
473-
// code: 'invalid-style-directive-modifier',
474-
// message: `Valid modifiers for style directives are: ${valid}`
475-
// })
476458
};
477459

478460
// interface is duplicated between here (used internally) and ./interfaces.js

packages/svelte/src/compiler/phases/1-parse/read/style.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,10 @@ function read_declaration(parser) {
476476

477477
const value = read_value(parser);
478478

479+
if (!value && !property.startsWith('--')) {
480+
error(parser.index, 'invalid-css-declaration');
481+
}
482+
479483
const end = parser.index;
480484

481485
if (!parser.match('}')) {

packages/svelte/src/compiler/phases/1-parse/state/element.js

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -268,22 +268,41 @@ export default function tag(parser) {
268268
if (name === 'script') {
269269
const content = read_script(parser, start, element.attributes);
270270

271-
if (content) {
272-
if (content.context === 'module') {
273-
if (current.module) error(start, 'duplicate-script-element');
274-
current.module = content;
275-
} else {
276-
if (current.instance) error(start, 'duplicate-script-element');
277-
current.instance = content;
271+
/** @type {import('#compiler').Comment | null} */
272+
let prev_comment = null;
273+
for (let i = current.fragment.nodes.length - 1; i >= 0; i--) {
274+
const node = current.fragment.nodes[i];
275+
276+
if (i === current.fragment.nodes.length - 1 && node.end !== start) {
277+
break;
278278
}
279+
280+
if (node.type === 'Comment') {
281+
prev_comment = node;
282+
break;
283+
} else if (node.type !== 'Text' || node.data.trim()) {
284+
break;
285+
}
286+
}
287+
if (prev_comment) {
288+
// We take advantage of the fact that the root will never have leadingComments set,
289+
// and set the previous comment to it so that the warning mechanism can later
290+
// inspect the root and see if there was a html comment before it silencing specific warnings.
291+
content.content.leadingComments = [{ type: 'Line', value: prev_comment.data }];
292+
}
293+
294+
if (content.context === 'module') {
295+
if (current.module) error(start, 'duplicate-script-element');
296+
current.module = content;
297+
} else {
298+
if (current.instance) error(start, 'duplicate-script-element');
299+
current.instance = content;
279300
}
280301
} else {
281302
const content = read_style(parser, start, element.attributes);
282303

283-
if (content) {
284-
if (current.css) error(start, 'duplicate-style-element');
285-
current.css = content;
286-
}
304+
if (current.css) error(start, 'duplicate-style-element');
305+
current.css = content;
287306
}
288307
return;
289308
}

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,25 @@ export function analyze_component(root, options) {
300300
declaration.initial.source.value === 'svelte/store'
301301
))
302302
) {
303+
let is_nested_store_subscription = false;
304+
for (const reference of references) {
305+
for (let i = reference.path.length - 1; i >= 0; i--) {
306+
const scope =
307+
scopes.get(reference.path[i]) ||
308+
module.scopes.get(reference.path[i]) ||
309+
instance.scopes.get(reference.path[i]);
310+
if (scope) {
311+
const owner = scope?.owner(store_name);
312+
is_nested_store_subscription =
313+
!!owner && owner !== module.scope && owner !== instance.scope;
314+
break;
315+
}
316+
}
317+
}
318+
if (is_nested_store_subscription) {
319+
error(references[0].node, 'illegal-store-subscription');
320+
}
321+
303322
if (options.runes !== false) {
304323
if (declaration === null && /[a-z]/.test(store_name[0])) {
305324
error(references[0].node, 'illegal-global', name);
@@ -442,6 +461,17 @@ export function analyze_component(root, options) {
442461
);
443462
}
444463

464+
for (const [name, binding] of instance.scope.declarations) {
465+
if (binding.kind === 'prop' && binding.node.name !== '$$props') {
466+
const references = binding.references.filter(
467+
(r) => r.node !== binding.node && r.path.at(-1)?.type !== 'ExportSpecifier'
468+
);
469+
if (!references.length && !instance.scope.declarations.has(`$${name}`)) {
470+
warn(warnings, binding.node, [], 'unused-export-let', name);
471+
}
472+
}
473+
}
474+
445475
analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements);
446476
}
447477

@@ -590,6 +620,17 @@ const legacy_scope_tweaker = {
590620

591621
state.reactive_statements.set(node, reactive_statement);
592622

623+
// Ideally this would be in the validation file, but that isn't possible because this visitor
624+
// calls "next" before setting the reactive statements.
625+
if (
626+
reactive_statement.dependencies.size &&
627+
[...reactive_statement.dependencies].every(
628+
(d) => d.scope === state.analysis.module.scope && d.declaration_kind !== 'const'
629+
)
630+
) {
631+
warn(state.analysis.warnings, node, path, 'module-script-reactive-declaration');
632+
}
633+
593634
if (
594635
node.body.type === 'ExpressionStatement' &&
595636
node.body.expression.type === 'AssignmentExpression'
@@ -710,7 +751,8 @@ const legacy_scope_tweaker = {
710751
if (
711752
binding.kind === 'state' ||
712753
binding.kind === 'frozen_state' ||
713-
(binding.kind === 'normal' && binding.declaration_kind === 'let')
754+
(binding.kind === 'normal' &&
755+
(binding.declaration_kind === 'let' || binding.declaration_kind === 'var'))
714756
) {
715757
binding.kind = 'prop';
716758
if (specifier.exported.name !== specifier.local.name) {

0 commit comments

Comments
 (0)