Skip to content

Commit d9369d8

Browse files
authored
fix: tighten up $ prefix validation (#13261)
Our current validation was both too lax and too strict: - too strict, because you can define a `$` variable in Svelte 4 if it's not at the top level - too strict, because you can define `$`-prefixed function parameters, but not give the parameter the name `$` - too lax, because you can define a `$`-prefixed variable if you're at least one level deep into a function. In runes mode, this should be an error This PR aligns the behavior, ensures this isn't a breaking change in legacy anymore, and makes the validation in runes mode more strict
1 parent 355730c commit d9369d8

File tree

12 files changed

+95
-18
lines changed

12 files changed

+95
-18
lines changed

.changeset/twelve-shrimps-run.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: tighten up `$` prefix validation

packages/svelte/src/compiler/phases/2-analyze/visitors/ClassDeclaration.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
/** @import { ClassDeclaration } from 'estree' */
22
/** @import { Context } from '../types' */
33
import * as w from '../../../warnings.js';
4+
import { validate_identifier_name } from './shared/utils.js';
45

56
/**
67
* @param {ClassDeclaration} node
78
* @param {Context} context
89
*/
910
export function ClassDeclaration(node, context) {
11+
if (context.state.analysis.runes) {
12+
validate_identifier_name(context.state.scope.get(node.id.name));
13+
}
14+
1015
// In modules, we allow top-level module scope only, in components, we allow the component scope,
1116
// which is function_depth of 1. With the exception of `new class` which is also not allowed at
1217
// component scope level either.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
/** @import { FunctionDeclaration } from 'estree' */
22
/** @import { Context } from '../types' */
33
import { visit_function } from './shared/function.js';
4+
import { validate_identifier_name } from './shared/utils.js';
45

56
/**
67
* @param {FunctionDeclaration} node
78
* @param {Context} context
89
*/
910
export function FunctionDeclaration(node, context) {
11+
if (context.state.analysis.runes) {
12+
validate_identifier_name(context.state.scope.get(node.id.name));
13+
}
14+
1015
visit_function(node, context);
1116
}

packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/** @import { Binding } from '#compiler' */
33
/** @import { Context } from '../types' */
44
import { get_rune } from '../../scope.js';
5-
import { ensure_no_module_import_conflict } from './shared/utils.js';
5+
import { ensure_no_module_import_conflict, validate_identifier_name } from './shared/utils.js';
66
import * as e from '../../../errors.js';
77
import { extract_paths } from '../../../utils/ast.js';
88
import { equal } from '../../../utils/assert.js';
@@ -17,6 +17,11 @@ export function VariableDeclarator(node, context) {
1717
if (context.state.analysis.runes) {
1818
const init = node.init;
1919
const rune = get_rune(init, context.state.scope);
20+
const paths = extract_paths(node.id);
21+
22+
for (const path of paths) {
23+
validate_identifier_name(context.state.scope.get(/** @type {Identifier} */ (path.node).name));
24+
}
2025

2126
// TODO feels like this should happen during scope creation?
2227
if (
@@ -26,7 +31,7 @@ export function VariableDeclarator(node, context) {
2631
rune === '$derived.by' ||
2732
rune === '$props'
2833
) {
29-
for (const path of extract_paths(node.id)) {
34+
for (const path of paths) {
3035
// @ts-ignore this fails in CI for some insane reason
3136
const binding = /** @type {Binding} */ (context.state.scope.get(path.node.name));
3237
binding.kind =

packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
/** @import { AssignmentExpression, Expression, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */
2-
/** @import { AST } from '#compiler' */
1+
/** @import { AssignmentExpression, Expression, Identifier, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */
2+
/** @import { AST, Binding } from '#compiler' */
33
/** @import { AnalysisState, Context } from '../../types' */
44
/** @import { Scope } from '../../../scope' */
55
/** @import { NodeLike } from '../../../../errors.js' */
@@ -196,3 +196,32 @@ export function is_pure(node, context) {
196196
// TODO add more cases (safe Svelte imports, etc)
197197
return false;
198198
}
199+
200+
/**
201+
* Checks if the name is valid, which it is when it's not starting with (or is) a dollar sign or if it's a function parameter.
202+
* The second argument is the depth of the scope, which is there for backwards compatibility reasons: In Svelte 4, you
203+
* were allowed to define `$`-prefixed variables anywhere below the top level of components. Once legacy mode is gone, this
204+
* argument can be removed / the call sites adjusted accordingly.
205+
* @param {Binding | null} binding
206+
* @param {number | undefined} [function_depth]
207+
*/
208+
export function validate_identifier_name(binding, function_depth) {
209+
if (!binding) return;
210+
211+
const declaration_kind = binding.declaration_kind;
212+
213+
if (
214+
declaration_kind !== 'synthetic' &&
215+
declaration_kind !== 'param' &&
216+
declaration_kind !== 'rest_param' &&
217+
(!function_depth || function_depth <= 1)
218+
) {
219+
const node = binding.node;
220+
221+
if (node.name === '$') {
222+
e.dollar_binding_invalid(node);
223+
} else if (node.name.startsWith('$')) {
224+
e.dollar_prefix_invalid(node);
225+
}
226+
}
227+
}

packages/svelte/src/compiler/phases/scope.js

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from '../utils/ast.js';
1515
import { is_reserved, is_rune } from '../../utils.js';
1616
import { determine_slot } from '../utils/slot.js';
17+
import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js';
1718

1819
export class Scope {
1920
/** @type {ScopeRoot} */
@@ -78,20 +79,6 @@ export class Scope {
7879
* @returns {Binding}
7980
*/
8081
declare(node, kind, declaration_kind, initial = null) {
81-
if (node.name === '$') {
82-
e.dollar_binding_invalid(node);
83-
}
84-
85-
if (
86-
node.name.startsWith('$') &&
87-
declaration_kind !== 'synthetic' &&
88-
declaration_kind !== 'param' &&
89-
declaration_kind !== 'rest_param' &&
90-
this.function_depth <= 1
91-
) {
92-
e.dollar_prefix_invalid(node);
93-
}
94-
9582
if (this.parent) {
9683
if (declaration_kind === 'var' && this.#porous) {
9784
return this.parent.declare(node, kind, declaration_kind);
@@ -123,6 +110,9 @@ export class Scope {
123110
prop_alias: null,
124111
metadata: null
125112
};
113+
114+
validate_identifier_name(binding, this.function_depth);
115+
126116
this.declarations.set(node.name, binding);
127117
this.root.conflicts.add(node.name);
128118
return binding;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
error: {
5+
code: 'dollar_binding_invalid',
6+
message: 'The $ name is reserved, and cannot be used for variables and imports',
7+
position: [108, 109]
8+
}
9+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<svelte:options runes={false} />
2+
3+
<script>
4+
function ok($) {}
5+
function ok2() {
6+
let $;
7+
}
8+
9+
// error
10+
let $;
11+
</script>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<svelte:options runes />
2+
3+
<script>
4+
function ok($) {}
5+
function error() {
6+
let $;
7+
}
8+
</script>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
error: {
5+
code: 'dollar_binding_invalid',
6+
message: 'The $ name is reserved, and cannot be used for variables and imports'
7+
}
8+
});
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
<svelte:options runes />
2+
13
<script>
24
let $;
35
</script>

0 commit comments

Comments
 (0)