mirror of https://github.com/sveltejs/svelte
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 savepull/12667/head
parent
8be7dd558b
commit
71c373d0a5
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
chore: internal compiler refactoring
|
File diff suppressed because it is too large
Load Diff
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();
|
||||
}
|
@ -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'
|
||||
);
|
||||
}
|
@ -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…
Reference in new issue