chore: refactor analysis (#12651)

* start moving visitors into separate modules

* remove unused code

* more

* more

* tidy up

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* alphabetize

* more

* fix

* more

* more

* consolidate

* more

* more

* more

* more

* more

* more

* more

* tweak

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* jfc what are we doing here

* more

* bizarre

* more

* more

* more

* more

* more

* more

* tidy

* one down

* dont merge

* hmm

* DRY

* more

* more

* tidy up

* tidy up

* add changeset, as this should have its own release

* tidy up

* oh i should probably hit save
pull/12667/head
Rich Harris 5 months ago committed by GitHub
parent 8be7dd558b
commit 71c373d0a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: internal compiler refactoring

File diff suppressed because it is too large Load Diff

@ -1,19 +1,11 @@
import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type {
ClassDirective,
ExpressionMetadata,
ExpressionTag,
OnDirective,
RenderTag,
SpreadAttribute,
SvelteNode,
ValidatedCompileOptions
} from '#compiler';
import type { ExpressionMetadata, RenderTag, SvelteNode, ValidatedCompileOptions } from '#compiler';
import type { LabeledStatement } from 'estree';
export interface AnalysisState {
scope: Scope;
scopes: Map<SvelteNode, Scope>;
analysis: ComponentAnalysis;
options: ValidatedCompileOptions;
ast_type: 'instance' | 'template' | 'module';
@ -27,9 +19,8 @@ export interface AnalysisState {
render_tag: null | RenderTag;
private_derived_state: string[];
function_depth: number;
}
export interface LegacyAnalysisState extends AnalysisState {
// legacy stuff
instance_scope: Scope;
reactive_statement: null | ReactiveStatement;
reactive_statements: Map<LabeledStatement, ReactiveStatement>;

File diff suppressed because it is too large Load Diff

@ -0,0 +1,11 @@
/** @import { ArrowFunctionExpression } from 'estree' */
/** @import { Context } from '../types' */
import { visit_function } from './shared/function.js';
/**
* @param {ArrowFunctionExpression} node
* @param {Context} context
*/
export function ArrowFunctionExpression(node, context) {
visit_function(node, context);
}

@ -0,0 +1,28 @@
/** @import { AssignmentExpression } from 'estree' */
/** @import { SvelteNode } from '#compiler' */
/** @import { Context } from '../types' */
import { extract_identifiers, object } from '../../../utils/ast.js';
import { validate_assignment } from './shared/utils.js';
/**
* @param {AssignmentExpression} node
* @param {Context} context
*/
export function AssignmentExpression(node, context) {
validate_assignment(node, node.left, context.state);
if (context.state.reactive_statement) {
const id = node.left.type === 'MemberExpression' ? object(node.left) : node.left;
if (id !== null) {
for (const id of extract_identifiers(node.left)) {
const binding = context.state.scope.get(id.name);
if (binding) {
context.state.reactive_statement.assignments.add(binding);
}
}
}
}
context.next();
}

@ -0,0 +1,213 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */
/** @import { Attribute, DelegatedEvent, RegularElement } from '#compiler' */
/** @import { Context } from '../types' */
import { DelegatedEvents, is_capture_event } from '../../../../constants.js';
import {
get_attribute_chunks,
get_attribute_expression,
is_event_attribute
} from '../../../utils/ast.js';
/**
* @param {Attribute} node
* @param {Context} context
*/
export function Attribute(node, context) {
context.next();
if (node.value !== true) {
for (const chunk of get_attribute_chunks(node.value)) {
if (chunk.type !== 'ExpressionTag') continue;
if (
chunk.expression.type === 'FunctionExpression' ||
chunk.expression.type === 'ArrowFunctionExpression'
) {
continue;
}
node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
}
if (is_event_attribute(node)) {
const parent = context.path.at(-1);
if (parent?.type === 'RegularElement' || parent?.type === 'SvelteElement') {
context.state.analysis.uses_event_attributes = true;
}
const expression = get_attribute_expression(node);
const delegated_event = get_delegated_event(node.name.slice(2), expression, context);
if (delegated_event !== null) {
if (delegated_event.type === 'hoistable') {
delegated_event.function.metadata.hoistable = true;
}
node.metadata.delegated = delegated_event;
}
}
}
}
/**
* Checks if given event attribute can be delegated/hoisted and returns the corresponding info if so
* @param {string} event_name
* @param {Expression | null} handler
* @param {Context} context
* @returns {null | DelegatedEvent}
*/
function get_delegated_event(event_name, handler, context) {
// Handle delegated event handlers. Bail-out if not a delegated event.
if (!handler || !DelegatedEvents.includes(event_name)) {
return null;
}
// If we are not working with a RegularElement, then bail-out.
const element = context.path.at(-1);
if (element?.type !== 'RegularElement') {
return null;
}
/** @type {DelegatedEvent} */
const non_hoistable = { type: 'non-hoistable' };
/** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | null} */
let target_function = null;
let binding = null;
if (element.metadata.has_spread) {
// event attribute becomes part of the dynamic spread array
return non_hoistable;
}
if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') {
target_function = handler;
} else if (handler.type === 'Identifier') {
binding = context.state.scope.get(handler.name);
if (context.state.analysis.module.scope.references.has(handler.name)) {
// If a binding with the same name is referenced in the module scope (even if not declared there), bail-out
return non_hoistable;
}
if (binding != null) {
for (const { path } of binding.references) {
const parent = path.at(-1);
if (parent == null) return non_hoistable;
const grandparent = path.at(-2);
/** @type {RegularElement | null} */
let element = null;
/** @type {string | null} */
let event_name = null;
if (parent.type === 'OnDirective') {
element = /** @type {RegularElement} */ (grandparent);
event_name = parent.name;
} else if (
parent.type === 'ExpressionTag' &&
grandparent?.type === 'Attribute' &&
is_event_attribute(grandparent)
) {
element = /** @type {RegularElement} */ (path.at(-3));
const attribute = /** @type {Attribute} */ (grandparent);
event_name = get_attribute_event_name(attribute.name);
}
if (element && event_name) {
if (
element.type !== 'RegularElement' ||
element.metadata.has_spread ||
!DelegatedEvents.includes(event_name)
) {
return non_hoistable;
}
} else if (parent.type !== 'FunctionDeclaration' && parent.type !== 'VariableDeclarator') {
return non_hoistable;
}
}
}
// If the binding is exported, bail-out
if (context.state.analysis.exports.find((node) => node.name === handler.name)) {
return non_hoistable;
}
if (binding !== null && binding.initial !== null && !binding.mutated && !binding.is_called) {
const binding_type = binding.initial.type;
if (
binding_type === 'ArrowFunctionExpression' ||
binding_type === 'FunctionDeclaration' ||
binding_type === 'FunctionExpression'
) {
target_function = binding.initial;
}
}
}
// If we can't find a function, bail-out
if (target_function == null) return non_hoistable;
// If the function is marked as non-hoistable, bail-out
if (target_function.metadata.hoistable === 'impossible') return non_hoistable;
// If the function has more than one arg, then bail-out
if (target_function.params.length > 1) return non_hoistable;
const visited_references = new Set();
const scope = target_function.metadata.scope;
for (const [reference] of scope.references) {
// Bail-out if the arguments keyword is used
if (reference === 'arguments') return non_hoistable;
// Bail-out if references a store subscription
if (scope.get(`$${reference}`)?.kind === 'store_sub') return non_hoistable;
const binding = scope.get(reference);
const local_binding = context.state.scope.get(reference);
// If we are referencing a binding that is shadowed in another scope then bail out.
if (local_binding !== null && binding !== null && local_binding.node !== binding.node) {
return non_hoistable;
}
// If we have multiple references to the same store using $ prefix, bail out.
if (
binding !== null &&
binding.kind === 'store_sub' &&
visited_references.has(reference.slice(1))
) {
return non_hoistable;
}
// If we reference the index within an each block, then bail-out.
if (binding !== null && binding.initial?.type === 'EachBlock') return non_hoistable;
if (
binding !== null &&
// Bail-out if the the binding is a rest param
(binding.declaration_kind === 'rest_param' ||
// Bail-out if we reference anything from the EachBlock (for now) that mutates in non-runes mode,
(((!context.state.analysis.runes && binding.kind === 'each') ||
// or any normal not reactive bindings that are mutated.
binding.kind === 'normal' ||
// or any reactive imports (those are rewritten) (can only happen in legacy mode)
binding.kind === 'legacy_reactive_import') &&
binding.mutated))
) {
return non_hoistable;
}
visited_references.add(reference);
}
return { type: 'hoistable', function: target_function };
}
/**
* @param {string} event_name
*/
function get_attribute_event_name(event_name) {
if (is_capture_event(event_name, 'include-on')) {
event_name = event_name.slice(0, -7);
}
event_name = event_name.slice(2);
return event_name;
}

@ -0,0 +1,42 @@
/** @import { AwaitBlock } from '#compiler' */
/** @import { Context } from '../types' */
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
import * as e from '../../../errors.js';
/**
* @param {AwaitBlock} node
* @param {Context} context
*/
export function AwaitBlock(node, context) {
validate_block_not_empty(node.pending, context);
validate_block_not_empty(node.then, context);
validate_block_not_empty(node.catch, context);
if (context.state.analysis.runes) {
validate_opening_tag(node, context.state, '#');
if (node.value) {
const start = /** @type {number} */ (node.value.start);
const match = context.state.analysis.source
.substring(start - 10, start)
.match(/{(\s*):then\s+$/);
if (match && match[1] !== '') {
e.block_unexpected_character({ start: start - 10, end: start }, ':');
}
}
if (node.error) {
const start = /** @type {number} */ (node.error.start);
const match = context.state.analysis.source
.substring(start - 10, start)
.match(/{(\s*):catch\s+$/);
if (match && match[1] !== '') {
e.block_unexpected_character({ start: start - 10, end: start }, ':');
}
}
}
context.next();
}

@ -0,0 +1,234 @@
/** @import { Attribute, BindDirective } from '#compiler' */
/** @import { Context } from '../types' */
import {
extract_all_identifiers_from_expression,
is_text_attribute,
object
} from '../../../utils/ast.js';
import { validate_no_const_assignment } from './shared/utils.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { binding_properties } from '../../bindings.js';
import { ContentEditableBindings, SVGElements } from '../../constants.js';
import fuzzymatch from '../../1-parse/utils/fuzzymatch.js';
/**
* @param {BindDirective} node
* @param {Context} context
*/
export function BindDirective(node, context) {
validate_no_const_assignment(node, node.expression, context.state.scope, true);
const assignee = node.expression;
const left = object(assignee);
if (left === null) {
e.bind_invalid_expression(node);
}
const binding = context.state.scope.get(left.name);
if (assignee.type === 'Identifier') {
// reassignment
if (
node.name !== 'this' && // bind:this also works for regular variables
(!binding ||
(binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'each' &&
binding.kind !== 'store_sub' &&
!binding.mutated))
) {
e.bind_invalid_value(node.expression);
}
if (binding?.kind === 'derived') {
e.constant_binding(node.expression, 'derived state');
}
if (context.state.analysis.runes && binding?.kind === 'each') {
e.each_item_invalid_assignment(node);
}
if (binding?.kind === 'snippet') {
e.snippet_parameter_assignment(node);
}
}
if (node.name === 'group') {
if (!binding) {
throw new Error('Cannot find declaration for bind:group');
}
// Traverse the path upwards and find all EachBlocks who are (indirectly) contributing to bind:group,
// i.e. one of their declarations is referenced in the binding. This allows group bindings to work
// correctly when referencing a variable declared in an EachBlock by using the index of the each block
// entries as keys.
const each_blocks = [];
const [keypath, expression_ids] = extract_all_identifiers_from_expression(node.expression);
let ids = expression_ids;
let i = context.path.length;
while (i--) {
const parent = context.path[i];
if (parent.type === 'EachBlock') {
const references = ids.filter((id) => parent.metadata.declarations.has(id.name));
if (references.length > 0) {
parent.metadata.contains_group_binding = true;
for (const binding of parent.metadata.references) {
binding.mutated = true;
}
each_blocks.push(parent);
ids = ids.filter((id) => !references.includes(id));
ids.push(...extract_all_identifiers_from_expression(parent.expression)[1]);
}
}
}
// The identifiers that make up the binding expression form they key for the binding group.
// If the same identifiers in the same order are used in another bind:group, they will be in the same group.
// (there's an edge case where `bind:group={a[i]}` will be in a different group than `bind:group={a[j]}` even when i == j,
// but this is a limitation of the current static analysis we do; it also never worked in Svelte 4)
const bindings = expression_ids.map((id) => context.state.scope.get(id.name));
let group_name;
outer: for (const [[key, b], group] of context.state.analysis.binding_groups) {
if (b.length !== bindings.length || key !== keypath) continue;
for (let i = 0; i < bindings.length; i++) {
if (bindings[i] !== b[i]) continue outer;
}
group_name = group;
}
if (!group_name) {
group_name = context.state.scope.root.unique('binding_group');
context.state.analysis.binding_groups.set([keypath, bindings], group_name);
}
node.metadata = {
binding_group_name: group_name,
parent_each_blocks: each_blocks
};
}
if (binding?.kind === 'each' && binding.metadata?.inside_rest) {
w.bind_invalid_each_rest(binding.node, binding.node.name);
}
const parent = context.path.at(-1);
if (
parent?.type === 'RegularElement' ||
parent?.type === 'SvelteElement' ||
parent?.type === 'SvelteWindow' ||
parent?.type === 'SvelteDocument' ||
parent?.type === 'SvelteBody'
) {
if (context.state.options.namespace === 'foreign' && node.name !== 'this') {
e.bind_invalid_name(node, node.name, 'Foreign elements only support `bind:this`');
}
if (node.name in binding_properties) {
const property = binding_properties[node.name];
if (property.valid_elements && !property.valid_elements.includes(parent.name)) {
e.bind_invalid_target(
node,
node.name,
property.valid_elements.map((valid_element) => `<${valid_element}>`).join(', ')
);
}
if (property.invalid_elements && property.invalid_elements.includes(parent.name)) {
const valid_bindings = Object.entries(binding_properties)
.filter(([_, binding_property]) => {
return (
binding_property.valid_elements?.includes(parent.name) ||
(!binding_property.valid_elements &&
!binding_property.invalid_elements?.includes(parent.name))
);
})
.map(([property_name]) => property_name)
.sort();
e.bind_invalid_name(
node,
node.name,
`Possible bindings for <${parent.name}> are ${valid_bindings.join(', ')}`
);
}
if (parent.name === 'input' && node.name !== 'this') {
const type = /** @type {Attribute | undefined} */ (
parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'type')
);
if (type && !is_text_attribute(type)) {
if (node.name !== 'value' || type.value === true) {
e.attribute_invalid_type(type);
}
} else {
if (node.name === 'checked' && type?.value[0].data !== 'checkbox') {
e.bind_invalid_target(node, node.name, '<input type="checkbox">');
}
if (node.name === 'files' && type?.value[0].data !== 'file') {
e.bind_invalid_target(node, node.name, '<input type="file">');
}
}
}
if (parent.name === 'select' && node.name !== 'this') {
const multiple = parent.attributes.find(
(a) =>
a.type === 'Attribute' &&
a.name === 'multiple' &&
!is_text_attribute(a) &&
a.value !== true
);
if (multiple) {
e.attribute_invalid_multiple(multiple);
}
}
if (node.name === 'offsetWidth' && SVGElements.includes(parent.name)) {
e.bind_invalid_target(
node,
node.name,
`non-<svg> elements. Use 'clientWidth' for <svg> instead`
);
}
if (ContentEditableBindings.includes(node.name)) {
const contenteditable = /** @type {Attribute} */ (
parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'contenteditable')
);
if (!contenteditable) {
e.attribute_contenteditable_missing(node);
} else if (!is_text_attribute(contenteditable) && contenteditable.value !== true) {
e.attribute_contenteditable_dynamic(contenteditable);
}
}
} else {
const match = fuzzymatch(node.name, Object.keys(binding_properties));
if (match) {
const property = binding_properties[match];
if (!property.valid_elements || property.valid_elements.includes(parent.name)) {
e.bind_invalid_name(node, node.name, `Did you mean '${match}'?`);
}
}
e.bind_invalid_name(node, node.name);
}
}
context.next();
}

@ -0,0 +1,209 @@
/** @import { CallExpression, VariableDeclarator } from 'estree' */
/** @import { SvelteNode } from '#compiler' */
/** @import { Context } from '../types' */
import { get_rune } from '../../scope.js';
import * as e from '../../../errors.js';
import { get_parent, unwrap_optional } from '../../../utils/ast.js';
import { is_safe_identifier } from './shared/utils.js';
/**
* @param {CallExpression} node
* @param {Context} context
*/
export function CallExpression(node, context) {
const parent = /** @type {SvelteNode} */ (get_parent(context.path, -1));
const rune = get_rune(node, context.state.scope);
switch (rune) {
case null:
if (!is_safe_identifier(node.callee, context.state.scope)) {
context.state.analysis.needs_context = true;
}
break;
case '$bindable':
if (node.arguments.length > 1) {
e.rune_invalid_arguments_length(node, '$bindable', 'zero or one arguments');
}
if (
parent.type !== 'AssignmentPattern' ||
context.path.at(-3)?.type !== 'ObjectPattern' ||
context.path.at(-4)?.type !== 'VariableDeclarator' ||
get_rune(
/** @type {VariableDeclarator} */ (context.path.at(-4)).init,
context.state.scope
) !== '$props'
) {
e.bindable_invalid_location(node);
}
break;
case '$host':
if (node.arguments.length > 0) {
e.rune_invalid_arguments(node, '$host');
} else if (context.state.ast_type === 'module' || !context.state.analysis.custom_element) {
e.host_invalid_placement(node);
}
break;
case '$props':
if (context.state.has_props_rune) {
e.props_duplicate(node);
}
context.state.has_props_rune = true;
if (
parent.type !== 'VariableDeclarator' ||
context.state.ast_type !== 'instance' ||
context.state.scope !== context.state.analysis.instance.scope
) {
e.props_invalid_placement(node);
}
if (node.arguments.length > 0) {
e.rune_invalid_arguments(node, rune);
}
break;
case '$state':
case '$state.frozen':
case '$derived':
case '$derived.by':
if (
parent.type !== 'VariableDeclarator' &&
!(parent.type === 'PropertyDefinition' && !parent.static && !parent.computed)
) {
e.state_invalid_placement(node, rune);
}
if ((rune === '$derived' || rune === '$derived.by') && node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
} else if (rune === '$state' && node.arguments.length > 1) {
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
}
break;
case '$effect':
case '$effect.pre':
if (parent.type !== 'ExpressionStatement') {
e.effect_invalid_placement(node);
}
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}
// `$effect` needs context because Svelte needs to know whether it should re-run
// effects that invalidate themselves, and that's determined by whether we're in runes mode
context.state.analysis.needs_context = true;
break;
case '$effect.tracking':
if (node.arguments.length !== 0) {
e.rune_invalid_arguments(node, rune);
}
break;
case '$effect.root':
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}
break;
case '$inspect':
if (node.arguments.length < 1) {
e.rune_invalid_arguments_length(node, rune, 'one or more arguments');
}
break;
case '$inspect().with':
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}
break;
case '$state.snapshot':
if (node.arguments.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
}
break;
case '$state.is':
if (node.arguments.length !== 2) {
e.rune_invalid_arguments_length(node, rune, 'exactly two arguments');
}
break;
}
if (context.state.expression && !is_known_safe_call(node, context)) {
context.state.expression.has_call = true;
context.state.expression.has_state = true;
}
if (context.state.render_tag) {
// Find out which of the render tag arguments contains this call expression
const arg_idx = unwrap_optional(context.state.render_tag.expression).arguments.findIndex(
(arg) => arg === node || context.path.includes(arg)
);
// -1 if this is the call expression of the render tag itself
if (arg_idx !== -1) {
context.state.render_tag.metadata.args_with_call_expression.add(arg_idx);
}
}
if (node.callee.type === 'Identifier') {
const binding = context.state.scope.get(node.callee.name);
if (binding !== null) {
binding.is_called = true;
}
}
// `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning
if (rune === '$inspect' || rune === '$derived') {
context.next({ ...context.state, function_depth: context.state.function_depth + 1 });
} else {
context.next();
}
}
/**
* @param {CallExpression} node
* @param {Context} context
* @returns {boolean}
*/
function is_known_safe_call(node, context) {
const callee = node.callee;
// String / Number / BigInt / Boolean casting calls
if (callee.type === 'Identifier') {
const name = callee.name;
const binding = context.state.scope.get(name);
if (
binding === null &&
(name === 'BigInt' || name === 'String' || name === 'Number' || name === 'Boolean')
) {
return true;
}
}
// TODO add more cases
return false;
}

@ -0,0 +1,27 @@
/** @import { ClassBody } from 'estree' */
/** @import { Context } from '../types' */
import { get_rune } from '../../scope.js';
/**
* @param {ClassBody} node
* @param {Context} context
*/
export function ClassBody(node, context) {
/** @type {string[]} */
const private_derived_state = [];
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
definition.key.type === 'PrivateIdentifier' &&
definition.value?.type === 'CallExpression'
) {
const rune = get_rune(definition.value, context.state.scope);
if (rune === '$derived' || rune === '$derived.by') {
private_derived_state.push(definition.key.name);
}
}
}
context.next({ ...context.state, private_derived_state });
}

@ -0,0 +1,20 @@
/** @import { ClassDeclaration } from 'estree' */
/** @import { Context } from '../types' */
import * as w from '../../../warnings.js';
/**
* @param {ClassDeclaration} node
* @param {Context} context
*/
export function ClassDeclaration(node, context) {
// In modules, we allow top-level module scope only, in components, we allow the component scope,
// which is function_depth of 1. With the exception of `new class` which is also not allowed at
// component scope level either.
const allowed_depth = context.state.ast_type === 'module' ? 0 : 1;
if (context.state.scope.function_depth > allowed_depth) {
w.perf_avoid_nested_class(node);
}
context.next();
}

@ -0,0 +1,10 @@
/** @import { ClassDirective } from '#compiler' */
/** @import { Context } from '../types' */
/**
* @param {ClassDirective} node
* @param {Context} context
*/
export function ClassDirective(node, context) {
context.next({ ...context.state, expression: node.metadata.expression });
}

@ -0,0 +1,20 @@
/** @import { Component } from '#compiler' */
/** @import { Context } from '../types' */
import { visit_component } from './shared/component.js';
/**
* @param {Component} node
* @param {Context} context
*/
export function Component(node, context) {
const binding = context.state.scope.get(
node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name
);
node.metadata.dynamic =
context.state.analysis.runes && // Svelte 4 required you to use svelte:component to switch components
binding !== null &&
(binding.kind !== 'normal' || node.name.includes('.'));
visit_component(node, context);
}

@ -0,0 +1,34 @@
/** @import { ConstTag } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { validate_opening_tag } from './shared/utils.js';
/**
* @param {ConstTag} node
* @param {Context} context
*/
export function ConstTag(node, context) {
if (context.state.analysis.runes) {
validate_opening_tag(node, context.state, '@');
}
const parent = context.path.at(-1);
const grand_parent = context.path.at(-2);
if (
parent?.type !== 'Fragment' ||
(grand_parent?.type !== 'IfBlock' &&
grand_parent?.type !== 'SvelteFragment' &&
grand_parent?.type !== 'Component' &&
grand_parent?.type !== 'SvelteComponent' &&
grand_parent?.type !== 'EachBlock' &&
grand_parent?.type !== 'AwaitBlock' &&
grand_parent?.type !== 'SnippetBlock' &&
((grand_parent?.type !== 'RegularElement' && grand_parent?.type !== 'SvelteElement') ||
!grand_parent.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')))
) {
e.const_tag_invalid_placement(node);
}
context.next();
}

@ -0,0 +1,15 @@
/** @import { DebugTag } from '#compiler' */
/** @import { Context } from '../types' */
import { validate_opening_tag } from './shared/utils.js';
/**
* @param {DebugTag} node
* @param {Context} context
*/
export function DebugTag(node, context) {
if (context.state.analysis.runes) {
validate_opening_tag(node, context.state, '@');
}
context.next();
}

@ -0,0 +1,29 @@
/** @import { EachBlock } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
/**
* @param {EachBlock} node
* @param {Context} context
*/
export function EachBlock(node, context) {
validate_opening_tag(node, context.state, '#');
validate_block_not_empty(node.body, context);
validate_block_not_empty(node.fallback, context);
const id = node.context;
if (id.type === 'Identifier' && (id.name === '$state' || id.name === '$derived')) {
// TODO weird that this is necessary
e.state_invalid_placement(node, id.name);
}
if (node.key) {
// treat `{#each items as item, i (i)}` as a normal indexed block, everything else as keyed
node.metadata.keyed =
node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index;
}
context.next();
}

@ -0,0 +1,15 @@
/** @import { ExportDefaultDeclaration, Node } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
/**
* @param {ExportDefaultDeclaration} node
* @param {Context} context
*/
export function ExportDefaultDeclaration(node, context) {
if (context.state.ast_type === 'instance') {
e.module_illegal_default_export(node);
}
context.next();
}

@ -0,0 +1,92 @@
/** @import { ExportNamedDeclaration, Identifier, Node } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { Context } from '../types' */
/** @import { Scope } from '../../scope' */
import * as e from '../../../errors.js';
import { extract_identifiers } from '../../../utils/ast.js';
/**
* @param {ExportNamedDeclaration} node
* @param {Context} context
*/
export function ExportNamedDeclaration(node, context) {
// visit children, so bindings are correctly initialised
context.next();
if (node.declaration?.type === 'VariableDeclaration') {
// in runes mode, forbid `export let`
if (
context.state.analysis.runes &&
context.state.ast_type === 'instance' &&
node.declaration.kind === 'let'
) {
e.legacy_export_invalid(node);
}
for (const declarator of node.declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) {
const binding = context.state.scope.get(id.name);
if (!binding) continue;
if (binding.kind === 'derived') {
e.derived_invalid_export(node);
}
if ((binding.kind === 'state' || binding.kind === 'frozen_state') && binding.reassigned) {
e.state_invalid_export(node);
}
}
}
}
if (context.state.ast_type === 'instance' && !context.state.analysis.runes) {
context.state.analysis.needs_props = true;
if (node.declaration) {
if (
node.declaration.type === 'FunctionDeclaration' ||
node.declaration.type === 'ClassDeclaration'
) {
context.state.analysis.exports.push({
name: /** @type {Identifier} */ (node.declaration.id).name,
alias: null
});
} else if (node.declaration.type === 'VariableDeclaration') {
if (node.declaration.kind === 'const') {
for (const declarator of node.declaration.declarations) {
for (const node of extract_identifiers(declarator.id)) {
context.state.analysis.exports.push({ name: node.name, alias: null });
}
}
} else {
for (const declarator of node.declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) {
const binding = /** @type {Binding} */ (context.state.scope.get(id.name));
binding.kind = 'bindable_prop';
}
}
}
}
}
}
if (context.state.analysis.runes) {
if (node.declaration && context.state.ast_type === 'instance') {
if (
node.declaration.type === 'FunctionDeclaration' ||
node.declaration.type === 'ClassDeclaration'
) {
context.state.analysis.exports.push({
name: /** @type {Identifier} */ (node.declaration.id).name,
alias: null
});
} else if (node.declaration.kind === 'const') {
for (const declarator of node.declaration.declarations) {
for (const node of extract_identifiers(declarator.id)) {
context.state.analysis.exports.push({ name: node.name, alias: null });
}
}
}
}
}
}

@ -0,0 +1,66 @@
/** @import { ExportSpecifier, Node } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { Context } from '../types' */
/** @import { Scope } from '../../scope' */
import * as e from '../../../errors.js';
/**
* @param {ExportSpecifier} node
* @param {Context} context
*/
export function ExportSpecifier(node, context) {
if (context.state.ast_type === 'instance') {
if (context.state.analysis.runes) {
context.state.analysis.exports.push({
name: node.local.name,
alias: node.exported.name
});
const binding = context.state.scope.get(node.local.name);
if (binding) binding.reassigned = true;
} else {
context.state.analysis.needs_props = true;
const binding = /** @type {Binding} */ (context.state.scope.get(node.local.name));
if (
binding !== null &&
(binding.kind === 'state' ||
binding.kind === 'frozen_state' ||
(binding.kind === 'normal' &&
(binding.declaration_kind === 'let' || binding.declaration_kind === 'var')))
) {
binding.kind = 'bindable_prop';
if (node.exported.name !== node.local.name) {
binding.prop_alias = node.exported.name;
}
} else {
context.state.analysis.exports.push({
name: node.local.name,
alias: node.exported.name
});
}
}
} else {
validate_export(node, context.state.scope, node.local.name);
}
}
/**
*
* @param {Node} node
* @param {Scope} scope
* @param {string} name
*/
function validate_export(node, scope, name) {
const binding = scope.get(name);
if (!binding) return;
if (binding.kind === 'derived') {
e.derived_invalid_export(node);
}
if ((binding.kind === 'state' || binding.kind === 'frozen_state') && binding.reassigned) {
e.state_invalid_export(node);
}
}

@ -0,0 +1,38 @@
/** @import { ExpressionStatement, ImportDeclaration } from 'estree' */
/** @import { Context } from '../types' */
import * as w from '../../../warnings.js';
/**
* @param {ExpressionStatement} node
* @param {Context} context
*/
export function ExpressionStatement(node, context) {
// warn on `new Component({ target: ... })` if imported from a `.svelte` file
if (
node.expression.type === 'NewExpression' &&
node.expression.callee.type === 'Identifier' &&
node.expression.arguments.length === 1 &&
node.expression.arguments[0].type === 'ObjectExpression' &&
node.expression.arguments[0].properties.some(
(p) => p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === 'target'
)
) {
const binding = context.state.scope.get(node.expression.callee.name);
if (binding?.kind === 'normal' && binding.declaration_kind === 'import') {
const declaration = /** @type {ImportDeclaration} */ (binding.initial);
// Theoretically someone could import a class from a `.svelte.js` module, but that's too rare to worry about
if (
/** @type {string} */ (declaration.source.value).endsWith('.svelte') &&
declaration.specifiers.find(
(s) => s.local.name === binding.node.name && s.type === 'ImportDefaultSpecifier'
)
) {
w.legacy_component_creation(node.expression);
}
}
}
context.next();
}

@ -0,0 +1,18 @@
/** @import { ExpressionTag } from '#compiler' */
/** @import { Context } from '../types' */
import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js';
import * as e from '../../../errors.js';
/**
* @param {ExpressionTag} node
* @param {Context} context
*/
export function ExpressionTag(node, context) {
if (node.parent && context.state.parent_element) {
if (!is_tag_valid_with_parent('#text', context.state.parent_element)) {
e.node_invalid_placement(node, '`{expression}`', context.state.parent_element);
}
}
context.next({ ...context.state, expression: node.metadata.expression });
}

@ -0,0 +1,11 @@
/** @import { FunctionDeclaration } from 'estree' */
/** @import { Context } from '../types' */
import { visit_function } from './shared/function.js';
/**
* @param {FunctionDeclaration} node
* @param {Context} context
*/
export function FunctionDeclaration(node, context) {
visit_function(node, context);
}

@ -0,0 +1,11 @@
/** @import { FunctionExpression } from 'estree' */
/** @import { Context } from '../types' */
import { visit_function } from './shared/function.js';
/**
* @param {FunctionExpression} node
* @param {Context} context
*/
export function FunctionExpression(node, context) {
visit_function(node, context);
}

@ -0,0 +1,15 @@
/** @import { HtmlTag } from '#compiler' */
/** @import { Context } from '../types' */
import { validate_opening_tag } from './shared/utils.js';
/**
* @param {HtmlTag} node
* @param {Context} context
*/
export function HtmlTag(node, context) {
if (context.state.analysis.runes) {
validate_opening_tag(node, context.state, '@');
}
context.next();
}

@ -0,0 +1,154 @@
/** @import { Expression, Identifier } from 'estree' */
/** @import { SvelteNode } from '#compiler' */
/** @import { Context } from '../types' */
import is_reference from 'is-reference';
import { Runes } from '../../constants.js';
import { should_proxy_or_freeze } from '../../3-transform/client/utils.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
/**
* @param {Identifier} node
* @param {Context} context
*/
export function Identifier(node, context) {
let i = context.path.length;
let parent = /** @type {Expression} */ (context.path[--i]);
if (!is_reference(node, parent)) {
return;
}
// If we are using arguments outside of a function, then throw an error
if (
node.name === 'arguments' &&
!context.path.some((n) => n.type === 'FunctionDeclaration' || n.type === 'FunctionExpression')
) {
e.invalid_arguments_usage(node);
}
// `$$slots` exists even in runes mode
if (node.name === '$$slots') {
context.state.analysis.uses_slots = true;
}
if (context.state.analysis.runes) {
if (
Runes.includes(/** @type {Runes[number]} */ (node.name)) &&
context.state.scope.get(node.name) === null &&
context.state.scope.get(node.name.slice(1)) === null
) {
/** @type {Expression} */
let current = node;
let name = node.name;
while (parent.type === 'MemberExpression') {
if (parent.computed) e.rune_invalid_computed_property(parent);
name += `.${/** @type {Identifier} */ (parent.property).name}`;
current = parent;
parent = /** @type {Expression} */ (context.path[--i]);
if (!Runes.includes(/** @type {Runes[number]} */ (name))) {
if (name === '$effect.active') {
e.rune_renamed(parent, '$effect.active', '$effect.tracking');
}
e.rune_invalid_name(parent, name);
}
}
if (parent.type !== 'CallExpression') {
e.rune_missing_parentheses(current);
}
}
}
let binding = context.state.scope.get(node.name);
if (!context.state.analysis.runes) {
if (node.name === '$$props') {
context.state.analysis.uses_props = true;
}
if (node.name === '$$restProps') {
context.state.analysis.uses_rest_props = true;
}
if (
binding?.kind === 'normal' &&
((binding.scope === context.state.instance_scope &&
binding.declaration_kind !== 'function') ||
binding.declaration_kind === 'import')
) {
if (binding.declaration_kind === 'import') {
if (
binding.mutated &&
// TODO could be more fine-grained - not every mention in the template implies a state binding
(context.state.reactive_statement || context.state.ast_type === 'template') &&
parent.type === 'MemberExpression'
) {
binding.kind = 'legacy_reactive_import';
}
} else if (
binding.mutated &&
// TODO could be more fine-grained - not every mention in the template implies a state binding
(context.state.reactive_statement || context.state.ast_type === 'template')
) {
binding.kind = 'state';
} else if (
context.state.reactive_statement &&
parent.type === 'AssignmentExpression' &&
parent.left === binding.node
) {
binding.kind = 'derived';
}
} else if (binding?.kind === 'each' && binding.mutated) {
// Ensure that the array is marked as reactive even when only its entries are mutated
let i = context.path.length;
while (i--) {
const ancestor = context.path[i];
if (
ancestor.type === 'EachBlock' &&
context.state.analysis.template.scopes.get(ancestor)?.declarations.get(node.name) ===
binding
) {
for (const binding of ancestor.metadata.references) {
if (binding.kind === 'normal') {
binding.kind = 'state';
}
}
break;
}
}
}
}
if (binding && binding.kind !== 'normal') {
if (context.state.expression) {
context.state.expression.dependencies.add(binding);
context.state.expression.has_state = true;
}
if (
context.state.analysis.runes &&
node !== binding.node &&
context.state.function_depth === binding.scope.function_depth &&
// If we have $state that can be proxied or frozen and isn't re-assigned, then that means
// it's likely not using a primitive value and thus this warning isn't that helpful.
((binding.kind === 'state' &&
(binding.reassigned ||
(binding.initial?.type === 'CallExpression' &&
binding.initial.arguments.length === 1 &&
binding.initial.arguments[0].type !== 'SpreadElement' &&
!should_proxy_or_freeze(binding.initial.arguments[0], context.state.scope)))) ||
binding.kind === 'frozen_state' ||
binding.kind === 'derived') &&
// We're only concerned with reads here
(parent.type !== 'AssignmentExpression' || parent.left !== node) &&
parent.type !== 'UpdateExpression'
) {
w.state_referenced_locally(node);
}
}
}

@ -0,0 +1,26 @@
/** @import { IfBlock } from '#compiler' */
/** @import { Context } from '../types' */
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
/**
* @param {IfBlock} node
* @param {Context} context
*/
export function IfBlock(node, context) {
validate_block_not_empty(node.consequent, context);
validate_block_not_empty(node.alternate, context);
if (context.state.analysis.runes) {
const parent = context.path.at(-1);
const expected =
context.path.at(-2)?.type === 'IfBlock' &&
parent?.type === 'Fragment' &&
parent.nodes.length === 1
? ':'
: '#';
validate_opening_tag(node, context.state, expected);
}
context.next();
}

@ -0,0 +1,30 @@
/** @import { ImportDeclaration } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
/**
* @param {ImportDeclaration} node
* @param {Context} context
*/
export function ImportDeclaration(node, context) {
if (context.state.analysis.runes) {
const source = /** @type {string} */ (node.source.value);
if (source.startsWith('svelte/internal')) {
e.import_svelte_internal_forbidden(node);
}
if (source === 'svelte') {
for (const specifier of node.specifiers) {
if (specifier.type === 'ImportSpecifier') {
if (
specifier.imported.name === 'beforeUpdate' ||
specifier.imported.name === 'afterUpdate'
) {
e.runes_mode_invalid_import(specifier, specifier.imported.name);
}
}
}
}
}
}

@ -0,0 +1,17 @@
/** @import { KeyBlock } from '#compiler' */
/** @import { Context } from '../types' */
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
/**
* @param {KeyBlock} node
* @param {Context} context
*/
export function KeyBlock(node, context) {
validate_block_not_empty(node.fragment, context);
if (context.state.analysis.runes) {
validate_opening_tag(node, context.state, '#');
}
context.next();
}

@ -0,0 +1,104 @@
/** @import { Expression, LabeledStatement } from 'estree' */
/** @import { ReactiveStatement, SvelteNode } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { extract_identifiers, object } from '../../../utils/ast.js';
import * as w from '../../../warnings.js';
/**
* @param {LabeledStatement} node
* @param {Context} context
*/
export function LabeledStatement(node, context) {
if (node.label.name === '$') {
const parent = /** @type {SvelteNode} */ (context.path.at(-1));
const is_reactive_statement =
context.state.ast_type === 'instance' && parent.type === 'Program';
if (is_reactive_statement) {
if (context.state.analysis.runes) {
e.legacy_reactive_statement_invalid(node);
}
// Find all dependencies of this `$: {...}` statement
/** @type {ReactiveStatement} */
const reactive_statement = {
assignments: new Set(),
dependencies: []
};
context.next({
...context.state,
reactive_statement,
function_depth: context.state.scope.function_depth + 1
});
// Every referenced binding becomes a dependency, unless it's on
// the left-hand side of an `=` assignment
for (const [name, nodes] of context.state.scope.references) {
const binding = context.state.scope.get(name);
if (binding === null) continue;
for (const { node, path } of nodes) {
/** @type {Expression} */
let left = node;
let i = path.length - 1;
let parent = /** @type {Expression} */ (path.at(i));
while (parent.type === 'MemberExpression') {
left = parent;
parent = /** @type {Expression} */ (path.at(--i));
}
if (
parent.type === 'AssignmentExpression' &&
parent.operator === '=' &&
parent.left === left
) {
continue;
}
reactive_statement.dependencies.push(binding);
break;
}
}
context.state.reactive_statements.set(node, reactive_statement);
if (
reactive_statement.dependencies.length &&
reactive_statement.dependencies.every(
(d) => d.scope === context.state.analysis.module.scope && d.declaration_kind !== 'const'
)
) {
w.reactive_declaration_module_script(node);
}
if (
node.body.type === 'ExpressionStatement' &&
node.body.expression.type === 'AssignmentExpression'
) {
let ids = extract_identifiers(node.body.expression.left);
if (node.body.expression.left.type === 'MemberExpression') {
const id = object(node.body.expression.left);
if (id !== null) {
ids = [id];
}
}
for (const id of ids) {
const binding = context.state.scope.get(id.name);
if (binding?.kind === 'legacy_reactive') {
// TODO does this include `let double; $: double = x * 2`?
binding.legacy_dependencies = Array.from(reactive_statement.dependencies);
}
}
}
} else if (!context.state.analysis.runes) {
w.reactive_declaration_invalid_placement(node);
}
}
context.next();
}

@ -0,0 +1,24 @@
/** @import { LetDirective } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
/**
* @param {LetDirective} node
* @param {Context} context
*/
export function LetDirective(node, context) {
const parent = context.path.at(-1);
if (
parent === undefined ||
(parent.type !== 'Component' &&
parent.type !== 'RegularElement' &&
parent.type !== 'SlotElement' &&
parent.type !== 'SvelteElement' &&
parent.type !== 'SvelteComponent' &&
parent.type !== 'SvelteSelf' &&
parent.type !== 'SvelteFragment')
) {
e.let_directive_invalid_placement(node);
}
}

@ -0,0 +1,27 @@
/** @import { MemberExpression } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { is_safe_identifier } from './shared/utils.js';
/**
* @param {MemberExpression} node
* @param {Context} context
*/
export function MemberExpression(node, context) {
if (node.object.type === 'Identifier' && node.property.type === 'Identifier') {
const binding = context.state.scope.get(node.object.name);
if (binding?.kind === 'rest_prop' && node.property.name.startsWith('$$')) {
e.props_illegal_name(node.property);
}
}
if (context.state.expression) {
context.state.expression.has_state = true;
}
if (!is_safe_identifier(node, context.state.scope)) {
context.state.analysis.needs_context = true;
}
context.next();
}

@ -0,0 +1,15 @@
/** @import { NewExpression } from 'estree' */
/** @import { Context } from '../types' */
import * as w from '../../../warnings.js';
/**
* @param {NewExpression} node
* @param {Context} context
*/
export function NewExpression(node, context) {
if (node.callee.type === 'ClassExpression' && context.state.scope.function_depth > 0) {
w.perf_avoid_inline_class(node);
}
context.next();
}

@ -0,0 +1,25 @@
/** @import { OnDirective } from '#compiler' */
/** @import { Context } from '../types' */
import * as w from '../../../warnings.js';
/**
* @param {OnDirective} node
* @param {Context} context
*/
export function OnDirective(node, context) {
if (context.state.analysis.runes) {
const parent_type = context.path.at(-1)?.type;
// Don't warn on component events; these might not be under the author's control so the warning would be unactionable
if (parent_type === 'RegularElement' || parent_type === 'SvelteElement') {
w.event_directive_deprecated(node, node.name);
}
}
const parent = context.path.at(-1);
if (parent?.type === 'SvelteElement' || parent?.type === 'RegularElement') {
context.state.analysis.event_directive_node ??= node;
}
context.next({ ...context.state, expression: node.metadata.expression });
}

@ -0,0 +1,164 @@
/** @import { RegularElement } from '#compiler' */
/** @import { Context } from '../types' */
import {
is_tag_valid_with_ancestor,
is_tag_valid_with_parent
} from '../../../../html-tree-validation.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { MathMLElements, SVGElements, VoidElements } from '../../constants.js';
import { create_attribute } from '../../nodes.js';
import { regex_starts_with_newline } from '../../patterns.js';
import { check_element } from './shared/a11y.js';
import { validate_element } from './shared/element.js';
/**
* @param {RegularElement} node
* @param {Context} context
*/
export function RegularElement(node, context) {
validate_element(node, context);
check_element(node, context.state);
context.state.analysis.elements.push(node);
// Special case: Move the children of <textarea> into a value attribute if they are dynamic
if (
context.state.options.namespace !== 'foreign' &&
node.name === 'textarea' &&
node.fragment.nodes.length > 0
) {
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute' && attribute.name === 'value') {
e.textarea_invalid_content(node);
}
}
if (node.fragment.nodes.length > 1 || node.fragment.nodes[0].type !== 'Text') {
const first = node.fragment.nodes[0];
if (first.type === 'Text') {
// The leading newline character needs to be stripped because of a qirk:
// It is ignored by browsers if the tag and its contents are set through
// innerHTML, but we're now setting it through the value property at which
// point it is _not_ ignored, so we need to strip it ourselves.
// see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
// see https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
first.data = first.data.replace(regex_starts_with_newline, '');
first.raw = first.raw.replace(regex_starts_with_newline, '');
}
node.attributes.push(
create_attribute(
'value',
/** @type {import('#compiler').Text} */ (node.fragment.nodes.at(0)).start,
/** @type {import('#compiler').Text} */ (node.fragment.nodes.at(-1)).end,
// @ts-ignore
node.fragment.nodes
)
);
node.fragment.nodes = [];
}
}
// Special case: single expression tag child of option element -> add "fake" attribute
// to ensure that value types are the same (else for example numbers would be strings)
if (
context.state.options.namespace !== 'foreign' &&
node.name === 'option' &&
node.fragment.nodes?.length === 1 &&
node.fragment.nodes[0].type === 'ExpressionTag' &&
!node.attributes.some(
(attribute) => attribute.type === 'Attribute' && attribute.name === 'value'
)
) {
const child = node.fragment.nodes[0];
node.attributes.push(create_attribute('value', child.start, child.end, [child]));
}
const binding = context.state.scope.get(node.name);
if (
binding !== null &&
binding.declaration_kind === 'import' &&
binding.references.length === 0
) {
w.component_name_lowercase(node, node.name);
}
node.metadata.has_spread = node.attributes.some(
(attribute) => attribute.type === 'SpreadAttribute'
);
if (context.state.options.namespace !== 'foreign') {
if (SVGElements.includes(node.name)) node.metadata.svg = true;
else if (MathMLElements.includes(node.name)) node.metadata.mathml = true;
}
if (context.state.parent_element) {
let past_parent = false;
let only_warn = false;
for (let i = context.path.length - 1; i >= 0; i--) {
const ancestor = context.path[i];
if (
ancestor.type === 'IfBlock' ||
ancestor.type === 'EachBlock' ||
ancestor.type === 'AwaitBlock' ||
ancestor.type === 'KeyBlock'
) {
// We're creating a separate template string inside blocks, which means client-side this would work
only_warn = true;
}
if (!past_parent) {
if (ancestor.type === 'RegularElement' && ancestor.name === context.state.parent_element) {
if (!is_tag_valid_with_parent(node.name, context.state.parent_element)) {
if (only_warn) {
w.node_invalid_placement_ssr(
node,
`\`<${node.name}>\``,
context.state.parent_element
);
} else {
e.node_invalid_placement(node, `\`<${node.name}>\``, context.state.parent_element);
}
}
past_parent = true;
}
} else if (ancestor.type === 'RegularElement') {
if (!is_tag_valid_with_ancestor(node.name, ancestor.name)) {
if (only_warn) {
w.node_invalid_placement_ssr(node, `\`<${node.name}>\``, ancestor.name);
} else {
e.node_invalid_placement(node, `\`<${node.name}>\``, ancestor.name);
}
}
} else if (
ancestor.type === 'Component' ||
ancestor.type === 'SvelteComponent' ||
ancestor.type === 'SvelteElement' ||
ancestor.type === 'SvelteSelf' ||
ancestor.type === 'SnippetBlock'
) {
break;
}
}
}
// Strip off any namespace from the beginning of the node name.
const node_name = node.name.replace(/[a-zA-Z-]*:/g, '');
if (
context.state.analysis.source[node.end - 2] === '/' &&
context.state.options.namespace !== 'foreign' &&
!VoidElements.includes(node_name) &&
!SVGElements.includes(node_name)
) {
w.element_invalid_self_closing_tag(node, node.name);
}
context.next({ ...context.state, parent_element: node.name });
}

@ -0,0 +1,37 @@
/** @import { RenderTag } from '#compiler' */
/** @import { Context } from '../types' */
import { unwrap_optional } from '../../../utils/ast.js';
import * as e from '../../../errors.js';
import { validate_opening_tag } from './shared/utils.js';
/**
* @param {RenderTag} node
* @param {Context} context
*/
export function RenderTag(node, context) {
validate_opening_tag(node, context.state, '@');
const callee = unwrap_optional(node.expression).callee;
node.metadata.dynamic =
callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal';
context.state.analysis.uses_render_tags = true;
const raw_args = unwrap_optional(node.expression).arguments;
for (const arg of raw_args) {
if (arg.type === 'SpreadElement') {
e.render_tag_invalid_spread_argument(arg);
}
}
if (
callee.type === 'MemberExpression' &&
callee.property.type === 'Identifier' &&
['bind', 'apply', 'call'].includes(callee.property.name)
) {
e.render_tag_invalid_call_expression(node);
}
context.next({ ...context.state, render_tag: node });
}

@ -0,0 +1,39 @@
/** @import { SlotElement } from '#compiler' */
/** @import { Context } from '../types' */
import { is_text_attribute } from '../../../utils/ast.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
/**
* @param {SlotElement} node
* @param {Context} context
*/
export function SlotElement(node, context) {
if (context.state.analysis.runes && !context.state.analysis.custom_element) {
w.slot_element_deprecated(node);
}
/** @type {string} */
let name = 'default';
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
if (attribute.name === 'name') {
if (!is_text_attribute(attribute)) {
e.slot_element_invalid_name(attribute);
}
name = attribute.value[0].data;
if (name === 'default') {
e.slot_element_invalid_name_default(attribute);
}
}
} else if (attribute.type !== 'SpreadAttribute' && attribute.type !== 'LetDirective') {
e.slot_element_invalid_attribute(attribute);
}
}
context.state.analysis.slot_names.set(name, node);
context.next();
}

@ -0,0 +1,55 @@
/** @import { SnippetBlock } from '#compiler' */
/** @import { Context } from '../types' */
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
import * as e from '../../../errors.js';
/**
* @param {SnippetBlock} node
* @param {Context} context
*/
export function SnippetBlock(node, context) {
validate_block_not_empty(node.body, context);
if (context.state.analysis.runes) {
validate_opening_tag(node, context.state, '#');
}
for (const arg of node.parameters) {
if (arg.type === 'RestElement') {
e.snippet_invalid_rest_parameter(arg);
}
}
context.next({ ...context.state, parent_element: null });
const { path } = context;
const parent = path.at(-2);
if (!parent) return;
if (
parent.type === 'Component' &&
parent.attributes.some(
(attribute) =>
(attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === node.expression.name
)
) {
e.snippet_shadowing_prop(node, node.expression.name);
}
if (node.expression.name !== 'children') return;
if (
parent.type === 'Component' ||
parent.type === 'SvelteComponent' ||
parent.type === 'SvelteSelf'
) {
if (
parent.fragment.nodes.some(
(node) => node.type !== 'SnippetBlock' && (node.type !== 'Text' || node.data.trim())
)
) {
e.snippet_conflict(node);
}
}
}

@ -0,0 +1,10 @@
/** @import { SpreadAttribute } from '#compiler' */
/** @import { Context } from '../types' */
/**
* @param {SpreadAttribute} node
* @param {Context} context
*/
export function SpreadAttribute(node, context) {
context.next({ ...context.state, expression: node.metadata.expression });
}

@ -0,0 +1,38 @@
/** @import { StyleDirective } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { get_attribute_chunks } from '../../../utils/ast.js';
/**
* @param {StyleDirective} node
* @param {Context} context
*/
export function StyleDirective(node, context) {
if (node.modifiers.length > 1 || (node.modifiers.length && node.modifiers[0] !== 'important')) {
e.style_directive_invalid_modifier(node);
}
if (node.value === true) {
// get the binding for node.name and change the binding to state
let binding = context.state.scope.get(node.name);
if (binding) {
if (!context.state.analysis.runes && binding.mutated) {
binding.kind = 'state';
}
if (binding.kind !== 'normal') {
node.metadata.expression.has_state = true;
}
}
} else {
context.next();
for (const chunk of get_attribute_chunks(node.value)) {
if (chunk.type !== 'ExpressionTag') continue;
node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
}
}
}

@ -0,0 +1,11 @@
/** @import { SvelteComponent } from '#compiler' */
/** @import { Context } from '../types' */
import { visit_component } from './shared/component.js';
/**
* @param {SvelteComponent} node
* @param {Context} context
*/
export function SvelteComponent(node, context) {
visit_component(node, context);
}

@ -0,0 +1,62 @@
/** @import { Attribute, SvelteElement, Text } from '#compiler' */
/** @import { Context } from '../types' */
import { namespace_mathml, namespace_svg } from '../../../../constants.js';
import { is_text_attribute } from '../../../utils/ast.js';
import { check_element } from './shared/a11y.js';
import { validate_element } from './shared/element.js';
/**
* @param {SvelteElement} node
* @param {Context} context
*/
export function SvelteElement(node, context) {
validate_element(node, context);
check_element(node, context.state);
context.state.analysis.elements.push(node);
const xmlns = /** @type {Attribute & { value: [Text] } | undefined} */ (
node.attributes.find(
(a) => a.type === 'Attribute' && a.name === 'xmlns' && is_text_attribute(a)
)
);
if (xmlns) {
node.metadata.svg = xmlns.value[0].data === namespace_svg;
node.metadata.mathml = xmlns.value[0].data === namespace_mathml;
} else {
let i = context.path.length;
while (i--) {
const ancestor = context.path[i];
if (
ancestor.type === 'Component' ||
ancestor.type === 'SvelteComponent' ||
ancestor.type === 'SvelteFragment' ||
ancestor.type === 'SnippetBlock'
) {
// Inside a slot or a snippet -> this resets the namespace, so assume the component namespace
node.metadata.svg = context.state.options.namespace === 'svg';
node.metadata.mathml = context.state.options.namespace === 'mathml';
break;
}
if (ancestor.type === 'SvelteElement' || ancestor.type === 'RegularElement') {
node.metadata.svg =
ancestor.type === 'RegularElement' && ancestor.name === 'foreignObject'
? false
: ancestor.metadata.svg;
node.metadata.mathml =
ancestor.type === 'RegularElement' && ancestor.name === 'foreignObject'
? false
: ancestor.metadata.mathml;
break;
}
}
}
context.next({ ...context.state, parent_element: null });
}

@ -0,0 +1,27 @@
/** @import { SvelteFragment } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { validate_slot_attribute } from './shared/attribute.js';
/**
* @param {SvelteFragment} node
* @param {Context} context
*/
export function SvelteFragment(node, context) {
const parent = context.path.at(-2);
if (parent?.type !== 'Component' && parent?.type !== 'SvelteComponent') {
e.svelte_fragment_invalid_placement(node);
}
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
if (attribute.name === 'slot') {
validate_slot_attribute(context, attribute);
}
} else if (attribute.type !== 'LetDirective') {
e.svelte_fragment_invalid_attribute(attribute);
}
}
context.next({ ...context.state, parent_element: null });
}

@ -0,0 +1,15 @@
/** @import { SvelteHead } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
/**
* @param {SvelteHead} node
* @param {Context} context
*/
export function SvelteHead(node, context) {
for (const attribute of node.attributes) {
e.svelte_head_illegal_attribute(attribute);
}
context.next();
}

@ -0,0 +1,11 @@
/** @import { SvelteSelf } from '#compiler' */
/** @import { Context } from '../types' */
import { visit_component } from './shared/component.js';
/**
* @param {SvelteSelf} node
* @param {Context} context
*/
export function SvelteSelf(node, context) {
visit_component(node, context);
}

@ -0,0 +1,17 @@
/** @import { Text } from '#compiler' */
/** @import { Context } from '../types' */
import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js';
import { regex_not_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js';
/**
* @param {Text} node
* @param {Context} context
*/
export function Text(node, context) {
if (node.parent && context.state.parent_element && regex_not_whitespace.test(node.data)) {
if (!is_tag_valid_with_parent('#text', context.state.parent_element)) {
e.node_invalid_placement(node, 'Text node', context.state.parent_element);
}
}
}

@ -0,0 +1,21 @@
/** @import { TitleElement } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
/**
* @param {TitleElement} node
* @param {Context} context
*/
export function TitleElement(node, context) {
for (const attribute of node.attributes) {
e.title_illegal_attribute(attribute);
}
for (const child of node.fragment.nodes) {
if (child.type !== 'Text' && child.type !== 'ExpressionTag') {
e.title_invalid_content(child);
}
}
context.next();
}

@ -0,0 +1,25 @@
/** @import { UpdateExpression } from 'estree' */
/** @import { Context } from '../types' */
import { object } from '../../../utils/ast.js';
import { validate_assignment } from './shared/utils.js';
/**
* @param {UpdateExpression} node
* @param {Context} context
*/
export function UpdateExpression(node, context) {
validate_assignment(node, node.argument, context.state);
if (context.state.reactive_statement) {
const id = node.argument.type === 'MemberExpression' ? object(node.argument) : node.argument;
if (id?.type === 'Identifier') {
const binding = context.state.scope.get(id.name);
if (binding) {
context.state.reactive_statement.assignments.add(binding);
}
}
}
context.next();
}

@ -0,0 +1,115 @@
/** @import { Expression, Identifier, Literal, VariableDeclarator } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { Context } from '../types' */
import { get_rune } from '../../scope.js';
import { ensure_no_module_import_conflict } from './shared/utils.js';
import * as e from '../../../errors.js';
import { extract_paths } from '../../../utils/ast.js';
import { equal } from '../../../utils/assert.js';
/**
* @param {VariableDeclarator} node
* @param {Context} context
*/
export function VariableDeclarator(node, context) {
ensure_no_module_import_conflict(node, context.state);
if (context.state.analysis.runes) {
const init = node.init;
const rune = get_rune(init, context.state.scope);
// TODO feels like this should happen during scope creation?
if (
rune === '$state' ||
rune === '$state.frozen' ||
rune === '$derived' ||
rune === '$derived.by' ||
rune === '$props'
) {
for (const path of extract_paths(node.id)) {
// @ts-ignore this fails in CI for some insane reason
const binding = /** @type {Binding} */ (context.state.scope.get(path.node.name));
binding.kind =
rune === '$state'
? 'state'
: rune === '$state.frozen'
? 'frozen_state'
: rune === '$derived' || rune === '$derived.by'
? 'derived'
: path.is_rest
? 'rest_prop'
: 'prop';
}
}
if (rune === '$props') {
if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') {
e.props_invalid_identifier(node);
}
context.state.analysis.needs_props = true;
if (node.id.type === 'Identifier') {
const binding = /** @type {Binding} */ (context.state.scope.get(node.id.name));
binding.initial = null; // else would be $props()
binding.kind = 'rest_prop';
} else {
equal(node.id.type, 'ObjectPattern');
for (const property of node.id.properties) {
if (property.type !== 'Property') continue;
if (property.computed) {
e.props_invalid_pattern(property);
}
if (property.key.type === 'Identifier' && property.key.name.startsWith('$$')) {
e.props_illegal_name(property);
}
const value =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
if (value.type !== 'Identifier') {
e.props_invalid_pattern(property);
}
const alias =
property.key.type === 'Identifier'
? property.key.name
: String(/** @type {Literal} */ (property.key).value);
let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null;
const binding = /** @type {Binding} */ (context.state.scope.get(value.name));
binding.prop_alias = alias;
// rewire initial from $props() to the actual initial value, stripping $bindable() if necessary
if (
initial?.type === 'CallExpression' &&
initial.callee.type === 'Identifier' &&
initial.callee.name === '$bindable'
) {
binding.initial = /** @type {Expression | null} */ (initial.arguments[0] ?? null);
binding.kind = 'bindable_prop';
} else {
binding.initial = initial;
}
}
}
}
} else {
if (node.init?.type === 'CallExpression') {
const callee = node.init.callee;
if (
callee.type === 'Identifier' &&
(callee.name === '$state' || callee.name === '$derived' || callee.name === '$props') &&
context.state.scope.get(callee.name)?.kind !== 'store_sub'
) {
e.rune_invalid_usage(node.init, callee.name);
}
}
}
context.next();
}

@ -1,5 +1,5 @@
/** @import { Visitors } from 'zimmerframe' */
/** @import { AnalysisState } from './types.js' */
/** @import { AnalysisState } from '../../types.js' */
/** @import { Attribute, SvelteNode, TemplateNode, RegularElement, SvelteElement } from '#compiler' */
/** @import { ARIARoleDefinitionKey, ARIARoleRelationConcept, ARIAProperty, ARIAPropertyDefinition, ARIARoleDefinition } from 'aria-query' */
import { roles as roles_map, aria, elementRoles } from 'aria-query';
@ -10,13 +10,13 @@ import {
regex_not_whitespace,
regex_starts_with_vowel,
regex_whitespaces
} from '../patterns.js';
import * as w from '../../warnings.js';
import fuzzymatch from '../1-parse/utils/fuzzymatch.js';
import { is_event_attribute, is_text_attribute } from '../../utils/ast.js';
import { ContentEditableBindings } from '../constants.js';
} from '../../../patterns.js';
import * as w from '../../../../warnings.js';
import fuzzymatch from '../../../1-parse/utils/fuzzymatch.js';
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
import { ContentEditableBindings } from '../../../constants.js';
import { walk } from 'zimmerframe';
import { list } from '../../utils/string.js';
import { list } from '../../../../utils/string.js';
const aria_roles = roles_map.keys();
const abstract_roles = aria_roles.filter((role) => roles_map.get(role)?.abstract);
@ -686,7 +686,7 @@ function get_static_text_value(attribute) {
* @param {RegularElement | SvelteElement} node
* @param {AnalysisState} state
*/
function check_element(node, state) {
export function check_element(node, state) {
// foreign namespace means elements can have completely different meanings, therefore we don't check them
if (state.options.namespace === 'foreign') return;
@ -1154,15 +1154,3 @@ function check_element(node, state) {
w.a11y_missing_content(node, node.name);
}
}
/**
* @type {Visitors<SvelteNode, AnalysisState>}
*/
export const a11y_validators = {
RegularElement(node, context) {
check_element(node, context.state);
},
SvelteElement(node, context) {
check_element(node, context.state);
}
};

@ -0,0 +1,123 @@
/** @import { Attribute, ElementLike } from '#compiler' */
/** @import { Context } from '../../types' */
import * as e from '../../../../errors.js';
import { is_text_attribute } from '../../../../utils/ast.js';
import * as w from '../../../../warnings.js';
import { is_custom_element_node } from '../../../nodes.js';
import { regex_only_whitespaces } from '../../../patterns.js';
/**
* @param {Attribute} attribute
*/
export function validate_attribute_name(attribute) {
if (
attribute.name.includes(':') &&
!attribute.name.startsWith('xmlns:') &&
!attribute.name.startsWith('xlink:') &&
!attribute.name.startsWith('xml:')
) {
w.attribute_illegal_colon(attribute);
}
}
/**
* @param {Attribute} attribute
* @param {ElementLike} parent
*/
export function validate_attribute(attribute, parent) {
if (
Array.isArray(attribute.value) &&
attribute.value.length === 1 &&
attribute.value[0].type === 'ExpressionTag' &&
(parent.type === 'Component' ||
parent.type === 'SvelteComponent' ||
parent.type === 'SvelteSelf' ||
(parent.type === 'RegularElement' && is_custom_element_node(parent)))
) {
w.attribute_quoted(attribute);
}
if (attribute.value === true || !Array.isArray(attribute.value) || attribute.value.length === 1) {
return;
}
const is_quoted = attribute.value.at(-1)?.end !== attribute.end;
if (!is_quoted) {
e.attribute_unquoted_sequence(attribute);
}
}
/**
* @param {Context} context
* @param {Attribute} attribute
* @param {boolean} is_component
*/
export function validate_slot_attribute(context, attribute, is_component = false) {
const parent = context.path.at(-2);
let owner = undefined;
if (parent?.type === 'SnippetBlock') {
if (!is_text_attribute(attribute)) {
e.slot_attribute_invalid(attribute);
}
return;
}
let i = context.path.length;
while (i--) {
const ancestor = context.path[i];
if (
!owner &&
(ancestor.type === 'Component' ||
ancestor.type === 'SvelteComponent' ||
ancestor.type === 'SvelteSelf' ||
ancestor.type === 'SvelteElement' ||
(ancestor.type === 'RegularElement' && is_custom_element_node(ancestor)))
) {
owner = ancestor;
}
}
if (owner) {
if (!is_text_attribute(attribute)) {
e.slot_attribute_invalid(attribute);
}
if (
owner.type === 'Component' ||
owner.type === 'SvelteComponent' ||
owner.type === 'SvelteSelf'
) {
if (owner !== parent) {
e.slot_attribute_invalid_placement(attribute);
}
const name = attribute.value[0].data;
if (context.state.component_slots.has(name)) {
e.slot_attribute_duplicate(attribute, name, owner.name);
}
context.state.component_slots.add(name);
if (name === 'default') {
for (const node of owner.fragment.nodes) {
if (node.type === 'Text' && regex_only_whitespaces.test(node.data)) {
continue;
}
if (node.type === 'RegularElement' || node.type === 'SvelteFragment') {
if (node.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')) {
continue;
}
}
e.slot_default_duplicate(node);
}
}
}
} else if (!is_component) {
e.slot_attribute_invalid_placement(attribute);
}
}

@ -0,0 +1,68 @@
/** @import { Component, SvelteComponent, SvelteSelf } from '#compiler' */
/** @import { Context } from '../../types' */
import * as e from '../../../../errors.js';
import { get_attribute_expression, is_expression_attribute } from '../../../../utils/ast.js';
import {
validate_attribute,
validate_attribute_name,
validate_slot_attribute
} from './attribute.js';
/**
* @param {Component | SvelteComponent | SvelteSelf} node
* @param {Context} context
*/
export function visit_component(node, context) {
for (const attribute of node.attributes) {
if (
attribute.type !== 'Attribute' &&
attribute.type !== 'SpreadAttribute' &&
attribute.type !== 'LetDirective' &&
attribute.type !== 'OnDirective' &&
attribute.type !== 'BindDirective'
) {
e.component_invalid_directive(attribute);
}
if (
attribute.type === 'OnDirective' &&
(attribute.modifiers.length > 1 || attribute.modifiers.some((m) => m !== 'once'))
) {
e.event_handler_invalid_component_modifier(attribute);
}
if (attribute.type === 'Attribute') {
if (context.state.analysis.runes) {
validate_attribute(attribute, node);
if (is_expression_attribute(attribute)) {
const expression = get_attribute_expression(attribute);
if (expression.type === 'SequenceExpression') {
let i = /** @type {number} */ (expression.start);
while (--i > 0) {
const char = context.state.analysis.source[i];
if (char === '(') break; // parenthesized sequence expressions are ok
if (char === '{') e.attribute_invalid_sequence_expression(expression);
}
}
}
}
validate_attribute_name(attribute);
if (attribute.name === 'slot') {
validate_slot_attribute(context, attribute, true);
}
}
if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
context.state.analysis.uses_component_bindings = true;
}
}
context.next({
...context.state,
parent_element: null,
component_slots: new Set()
});
}

@ -0,0 +1,149 @@
/** @import { Component, RegularElement, SvelteComponent, SvelteElement, SvelteSelf, TransitionDirective } from '#compiler' */
/** @import { Context } from '../../types' */
import { get_attribute_expression, is_expression_attribute } from '../../../../utils/ast.js';
import { regex_illegal_attribute_character } from '../../../patterns.js';
import * as e from '../../../../errors.js';
import * as w from '../../../../warnings.js';
import { EventModifiers } from '../../../constants.js';
import {
validate_attribute,
validate_attribute_name,
validate_slot_attribute
} from './attribute.js';
/**
* @param {import('#compiler').RegularElement | SvelteElement} node
* @param {Context} context
*/
export function validate_element(node, context) {
let has_animate_directive = false;
/** @type {TransitionDirective | null} */
let in_transition = null;
/** @type {TransitionDirective | null} */
let out_transition = null;
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
const is_expression = is_expression_attribute(attribute);
if (context.state.analysis.runes) {
validate_attribute(attribute, node);
if (is_expression) {
const expression = get_attribute_expression(attribute);
if (expression.type === 'SequenceExpression') {
let i = /** @type {number} */ (expression.start);
while (--i > 0) {
const char = context.state.analysis.source[i];
if (char === '(') break; // parenthesized sequence expressions are ok
if (char === '{') e.attribute_invalid_sequence_expression(expression);
}
}
}
}
if (regex_illegal_attribute_character.test(attribute.name)) {
e.attribute_invalid_name(attribute, attribute.name);
}
if (attribute.name.startsWith('on') && attribute.name.length > 2) {
if (!is_expression) {
e.attribute_invalid_event_handler(attribute);
}
const value = get_attribute_expression(attribute);
if (
value.type === 'Identifier' &&
value.name === attribute.name &&
!context.state.scope.get(value.name)
) {
w.attribute_global_event_reference(attribute, attribute.name);
}
}
if (attribute.name === 'slot') {
/** @type {RegularElement | SvelteElement | Component | SvelteComponent | SvelteSelf | undefined} */
validate_slot_attribute(context, attribute);
}
if (attribute.name === 'is' && context.state.options.namespace !== 'foreign') {
w.attribute_avoid_is(attribute);
}
const correct_name = react_attributes.get(attribute.name);
if (correct_name) {
w.attribute_invalid_property_name(attribute, attribute.name, correct_name);
}
validate_attribute_name(attribute);
} else if (attribute.type === 'AnimateDirective') {
const parent = context.path.at(-2);
if (parent?.type !== 'EachBlock') {
e.animation_invalid_placement(attribute);
} else if (!parent.key) {
e.animation_missing_key(attribute);
} else if (
parent.body.nodes.filter(
(n) =>
n.type !== 'Comment' &&
n.type !== 'ConstTag' &&
(n.type !== 'Text' || n.data.trim() !== '')
).length > 1
) {
e.animation_invalid_placement(attribute);
}
if (has_animate_directive) {
e.animation_duplicate(attribute);
} else {
has_animate_directive = true;
}
} else if (attribute.type === 'TransitionDirective') {
const existing = /** @type {TransitionDirective | null} */ (
(attribute.intro && in_transition) || (attribute.outro && out_transition)
);
if (existing) {
const a = existing.intro ? (existing.outro ? 'transition' : 'in') : 'out';
const b = attribute.intro ? (attribute.outro ? 'transition' : 'in') : 'out';
if (a === b) {
e.transition_duplicate(attribute, a);
} else {
e.transition_conflict(attribute, a, b);
}
}
if (attribute.intro) in_transition = attribute;
if (attribute.outro) out_transition = attribute;
} else if (attribute.type === 'OnDirective') {
let has_passive_modifier = false;
let conflicting_passive_modifier = '';
for (const modifier of attribute.modifiers) {
if (!EventModifiers.includes(modifier)) {
const list = `${EventModifiers.slice(0, -1).join(', ')} or ${EventModifiers.at(-1)}`;
e.event_handler_invalid_modifier(attribute, list);
}
if (modifier === 'passive') {
has_passive_modifier = true;
} else if (modifier === 'nonpassive' || modifier === 'preventDefault') {
conflicting_passive_modifier = modifier;
}
if (has_passive_modifier && conflicting_passive_modifier) {
e.event_handler_invalid_modifier_combination(
attribute,
'passive',
conflicting_passive_modifier
);
}
}
}
}
}
const react_attributes = new Map([
['className', 'class'],
['htmlFor', 'for']
]);

@ -0,0 +1,21 @@
/** @import { ArrowFunctionExpression, FunctionDeclaration, FunctionExpression } from 'estree' */
/** @import { Context } from '../../types' */
/**
* @param {ArrowFunctionExpression | FunctionExpression | FunctionDeclaration} node
* @param {Context} context
*/
export function visit_function(node, context) {
// TODO retire this in favour of a more general solution based on bindings
node.metadata = {
// module context -> already hoisted
hoistable: context.state.ast_type === 'module' ? 'impossible' : false,
hoistable_params: [],
scope: context.state.scope
};
context.next({
...context.state,
function_depth: context.state.function_depth + 1
});
}

@ -0,0 +1,167 @@
/** @import { AssignmentExpression, Expression, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */
/** @import { Fragment } from '#compiler' */
/** @import { AnalysisState, Context } from '../../types' */
/** @import { Scope } from '../../../scope' */
/** @import { NodeLike } from '../../../../errors.js' */
import * as e from '../../../../errors.js';
import { extract_identifiers } from '../../../../utils/ast.js';
import * as w from '../../../../warnings.js';
/**
* @param {AssignmentExpression | UpdateExpression} node
* @param {Pattern | Expression} argument
* @param {AnalysisState} state
*/
export function validate_assignment(node, argument, state) {
validate_no_const_assignment(node, argument, state.scope, false);
if (argument.type === 'Identifier') {
const binding = state.scope.get(argument.name);
if (state.analysis.runes) {
if (binding?.kind === 'derived') {
e.constant_assignment(node, 'derived state');
}
if (binding?.kind === 'each') {
e.each_item_invalid_assignment(node);
}
}
if (binding?.kind === 'snippet') {
e.snippet_parameter_assignment(node);
}
}
let object = /** @type {Expression | Super} */ (argument);
/** @type {Expression | PrivateIdentifier | null} */
let property = null;
while (object.type === 'MemberExpression') {
property = object.property;
object = object.object;
}
if (object.type === 'ThisExpression' && property?.type === 'PrivateIdentifier') {
if (state.private_derived_state.includes(property.name)) {
e.constant_assignment(node, 'derived state');
}
}
}
/**
* @param {NodeLike} node
* @param {Pattern | Expression} argument
* @param {Scope} scope
* @param {boolean} is_binding
*/
export function validate_no_const_assignment(node, argument, scope, is_binding) {
if (argument.type === 'ArrayPattern') {
for (const element of argument.elements) {
if (element) {
validate_no_const_assignment(node, element, scope, is_binding);
}
}
} else if (argument.type === 'ObjectPattern') {
for (const element of argument.properties) {
if (element.type === 'Property') {
validate_no_const_assignment(node, element.value, scope, is_binding);
}
}
} else if (argument.type === 'Identifier') {
const binding = scope.get(argument.name);
if (binding?.declaration_kind === 'const' && binding.kind !== 'each') {
// e.invalid_const_assignment(
// node,
// is_binding,
// // This takes advantage of the fact that we don't assign initial for let directives and then/catch variables.
// // If we start doing that, we need another property on the binding to differentiate, or give up on the more precise error message.
// binding.kind !== 'state' &&
// binding.kind !== 'frozen_state' &&
// (binding.kind !== 'normal' || !binding.initial)
// );
// TODO have a more specific error message for assignments to things like `{:then foo}`
const thing = 'constant';
if (is_binding) {
e.constant_binding(node, thing);
} else {
e.constant_assignment(node, thing);
}
}
}
}
/**
* Validates that the opening of a control flow block is `{` immediately followed by the expected character.
* In legacy mode whitespace is allowed inbetween. TODO remove once legacy mode is gone and move this into parser instead.
* @param {{start: number; end: number}} node
* @param {AnalysisState} state
* @param {string} expected
*/
export function validate_opening_tag(node, state, expected) {
if (state.analysis.source[node.start + 1] !== expected) {
// avoid a sea of red and only mark the first few characters
e.block_unexpected_character({ start: node.start, end: node.start + 5 }, expected);
}
}
/**
* @param {Fragment | null | undefined} node
* @param {Context} context
*/
export 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()) {
w.block_empty(node.nodes[0]);
}
}
/**
* @param {VariableDeclarator} node
* @param {AnalysisState} state
*/
export function ensure_no_module_import_conflict(node, state) {
const ids = extract_identifiers(node.id);
for (const id of ids) {
if (
state.ast_type === 'instance' &&
state.scope === state.analysis.instance.scope &&
state.analysis.module.scope.get(id.name)?.declaration_kind === 'import'
) {
// TODO fix the message here
e.declaration_duplicate_module_import(node.id);
}
}
}
/**
* A 'safe' identifier means that the `foo` in `foo.bar` or `foo()` will not
* call functions that require component context to exist
* @param {Expression | Super} expression
* @param {Scope} scope
*/
export function is_safe_identifier(expression, scope) {
let node = expression;
while (node.type === 'MemberExpression') node = node.object;
if (node.type !== 'Identifier') return false;
const binding = scope.get(node.name);
if (!binding) return true;
if (binding.kind === 'store_sub') {
return is_safe_identifier({ name: node.name.slice(1), type: 'Identifier' }, scope);
}
return (
binding.declaration_kind !== 'import' &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'rest_prop'
);
}

@ -9,7 +9,7 @@ import type {
SvelteNode,
SvelteOptions
} from '#compiler';
import type { Identifier, LabeledStatement, Program, Statement, VariableDeclaration } from 'estree';
import type { Identifier, LabeledStatement, Program, VariableDeclaration } from 'estree';
import type { Scope, ScopeRoot } from './scope.js';
export interface Js {

@ -8,6 +8,7 @@ const overrides = {
}
};
// TODO get rid of this
/**
* @template {{ type: string }} T
* @template U

@ -139,6 +139,7 @@ export function extract_identifiers(pattern) {
/**
* Extracts all identifiers and a stringified keypath from an expression.
* TODO replace this with `expression.dependencies`
* @param {ESTree.Expression} expr
* @returns {[keypath: string, ids: ESTree.Identifier[]]}
*/

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'props_invalid_placement',
message:
'`$props()` can only be used at the top level of components as a variable declaration initializer'
}
});
Loading…
Cancel
Save