move stuff around

tidy-up-analysis
Rich Harris 2 years ago
parent 72418ecfac
commit 65ee069218

@ -15,7 +15,8 @@ import { DelegatedEvents, ReservedKeywords, Runes, SVGElements } from '../consta
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js'; import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import { merge } from '../visitors.js'; import { merge } from '../visitors.js';
import Stylesheet from './css/Stylesheet.js'; import Stylesheet from './css/Stylesheet.js';
import { validation_legacy, validation_runes, validation_runes_js } from './visitors/validate.js'; import { validation_legacy, validation_runes } from './visitors/validate.js';
import { validate_javascript_runes } from './visitors/validate-javascript-runes.js';
import { warn } from '../../warnings.js'; import { warn } from '../../warnings.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js'; import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
import { regex_starts_with_newline } from '../patterns.js'; import { regex_starts_with_newline } from '../patterns.js';

@ -1,4 +1,5 @@
import { error } from '../../errors.js'; import { error } from '../../errors.js';
import { get_rune } from '../scope';
/** /**
* @param {import('../../errors.js').NodeLike} node * @param {import('../../errors.js').NodeLike} node
@ -53,3 +54,43 @@ export function validate_assignment(node, argument, state) {
} }
} }
} }
/**
*
* @param {import('estree').Node} node
* @param {import('../scope.js').Scope} scope
* @param {string} name
*/
export function validate_export(node, scope, name) {
const binding = scope.get(name);
if (binding && (binding.kind === 'derived' || binding.kind === 'state')) {
error(node, 'invalid-rune-export', `$${binding.kind}`);
}
}
/**
* @param {import('estree').CallExpression} node
* @param {import('../scope').Scope} scope
* @param {import('#compiler').SvelteNode[]} path
* @returns
*/
export function validate_call_expression(node, scope, path) {
const rune = get_rune(node, scope);
if (rune === null) return;
if (rune === '$props' && path.at(-1)?.type !== 'VariableDeclarator') {
error(node, 'invalid-props-location');
} else if (
(rune === '$state' || rune === '$derived') &&
path.at(-1)?.type !== 'VariableDeclarator' &&
path.at(-1)?.type !== 'PropertyDefinition'
) {
error(node, rune === '$derived' ? 'invalid-derived-location' : 'invalid-state-location');
} else if (rune === '$effect') {
if (path.at(-1)?.type !== 'ExpressionStatement') {
error(node, 'invalid-effect-location');
} else if (node.arguments.length !== 1) {
error(node, 'invalid-rune-args-length', '$effect', [1]);
}
}
}

@ -0,0 +1,73 @@
import { error } from '../../../errors.js';
import { extract_identifiers } from '../../../utils/ast.js';
import { get_rune } from '../../scope.js';
import { validate_assignment, validate_call_expression, validate_export } from '../utils.js';
/**
* Validation that applies to .svelte.js files (TODO and <script context="module">?)
* @type {import('../types').Visitors}
*/
export const validate_javascript_runes = {
ExportSpecifier(node, { state }) {
validate_export(node, state.scope, node.local.name);
},
ExportNamedDeclaration(node, { state, next }) {
if (node.declaration?.type !== 'VariableDeclaration') return;
// visit children, so bindings are correctly initialised
next();
for (const declarator of node.declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) {
validate_export(node, state.scope, id.name);
}
}
},
CallExpression(node, { state, path }) {
validate_call_expression(node, state.scope, path);
},
VariableDeclarator(node, { state }) {
const init = node.init;
const rune = get_rune(init, state.scope);
if (rune === null) return;
const args = /** @type {import('estree').CallExpression} */ (init).arguments;
if (rune === '$derived' && args.length !== 1) {
error(node, 'invalid-rune-args-length', '$derived', [1]);
} else if (rune === '$state' && args.length > 1) {
error(node, 'invalid-rune-args-length', '$state', [0, 1]);
} else if (rune === '$props') {
error(node, 'invalid-props-location');
}
},
AssignmentExpression(node, { state }) {
validate_assignment(node, node.left, state);
},
UpdateExpression(node, { state }) {
validate_assignment(node, node.argument, state);
},
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') {
private_derived_state.push(definition.key.name);
}
}
}
context.next({
...context.state,
private_derived_state
});
}
};

@ -1,7 +1,10 @@
import { error } from '../../../errors.js'; import { error } from '../../../errors.js';
import { validate_assignment } from '../utils.js'; import { validate_assignment } from '../utils.js';
/** @type {import('../types').Visitors} */ /**
* Validation that only applies in non-runes mode
* @type {import('../types').Visitors}
*/
export const validate_legacy = { export const validate_legacy = {
VariableDeclarator(node) { VariableDeclarator(node) {
if (node.init?.type !== 'CallExpression') return; if (node.init?.type !== 'CallExpression') return;

@ -0,0 +1,94 @@
import { error } from '../../../errors.js';
import { get_rune } from '../../scope.js';
import { validate_assignment, validate_call_expression, validate_export } from '../utils.js';
import { validate_javascript_runes } from './validate-javascript-runes.js';
/** @type {import('../types').Visitors} */
export const validate_runes = {
AssignmentExpression(node, { state, path }) {
const parent = path.at(-1);
if (parent && parent.type === 'ConstTag') return;
validate_assignment(node, node.left, state);
},
UpdateExpression(node, { state }) {
validate_assignment(node, node.argument, state);
},
LabeledStatement(node, { path }) {
if (node.label.name !== '$' || path.at(-1)?.type !== 'Program') return;
error(node, 'invalid-legacy-reactive-statement');
},
ExportNamedDeclaration(node, { state }) {
if (node.declaration?.type !== 'VariableDeclaration') return;
if (node.declaration.kind !== 'let') return;
if (state.analysis.instance.scope !== state.scope) return;
error(node, 'invalid-legacy-export');
},
ExportSpecifier(node, { state }) {
validate_export(node, state.scope, node.local.name);
},
CallExpression(node, { state, path }) {
validate_call_expression(node, state.scope, path);
},
EachBlock(node, { next, state }) {
const context = node.context;
if (
context.type === 'Identifier' &&
(context.name === '$state' || context.name === '$derived')
) {
error(
node,
context.name === '$derived' ? 'invalid-derived-location' : 'invalid-state-location'
);
}
next({ ...state });
},
VariableDeclarator(node, { state }) {
const init = node.init;
const rune = get_rune(init, state.scope);
if (rune === null) return;
const args = /** @type {import('estree').CallExpression} */ (init).arguments;
if (rune === '$derived' && args.length !== 1) {
error(node, 'invalid-rune-args-length', '$derived', [1]);
} else if (rune === '$state' && args.length > 1) {
error(node, 'invalid-rune-args-length', '$state', [0, 1]);
} else if (rune === '$props') {
if (state.has_props_rune) {
error(node, 'duplicate-props-rune');
}
state.has_props_rune = true;
if (args.length > 0) {
error(node, 'invalid-rune-args-length', '$props', [0]);
}
if (node.id.type !== 'ObjectPattern') {
error(node, 'invalid-props-id');
}
if (state.scope !== state.analysis.instance.scope) {
error(node, 'invalid-props-location');
}
for (const property of node.id.properties) {
if (property.type === 'Property') {
if (property.computed) {
error(property, 'invalid-props-pattern');
}
const value =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
if (value.type !== 'Identifier') {
error(property, 'invalid-props-pattern');
}
}
}
}
},
// TODO move this
ClassBody: validate_javascript_runes.ClassBody
};

@ -0,0 +1,437 @@
import { error } from '../../../errors.js';
import { is_text_attribute } from '../../../utils/ast.js';
import { warn } from '../../../warnings.js';
import fuzzymatch from '../../1-parse/utils/fuzzymatch.js';
import { binding_properties } from '../../bindings.js';
import { SVGElements } from '../../constants.js';
import { is_custom_element_node } from '../../nodes.js';
import { regex_not_whitespace, regex_only_whitespaces } from '../../patterns.js';
import { validate_no_const_assignment } from '../utils.js';
/**
* @param {import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf} node
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('../types.js').AnalysisState>} context
*/
function validate_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'
) {
error(attribute, 'invalid-component-directive');
}
}
context.next({
...context.state,
parent_element: null,
component_slots: new Set()
});
}
/**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('../types.js').AnalysisState>} context
*/
function validate_element(node, context) {
for (const attribute of node.attributes) {
if (
attribute.type === 'Attribute' &&
attribute.name === 'is' &&
context.state.options.namespace !== 'foreign'
) {
warn(context.state.analysis.warnings, attribute, context.path, 'avoid-is');
}
if (attribute.type === 'Attribute' && attribute.name === 'slot') {
/** @type {import('#compiler').RegularElement | import('#compiler').SvelteElement | import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf | undefined} */
validate_slot_attribute(context, attribute);
}
}
}
/**
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('../types.js').AnalysisState>} context
* @param {import('#compiler').Attribute} attribute
*/
function validate_slot_attribute(context, attribute) {
let owner = undefined;
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)) {
error(attribute, 'invalid-slot-attribute');
}
if (owner.type === 'Component' || owner.type === 'SvelteComponent') {
if (owner !== context.path.at(-2)) {
error(attribute, 'invalid-slot-placement');
}
}
const name = attribute.value[0].data;
if (context.state.component_slots.has(name)) {
error(attribute, 'duplicate-slot-name', 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;
}
}
error(node, 'invalid-default-slot-content');
}
}
} else {
error(attribute, 'invalid-slot-placement');
}
}
// https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags
const implied_end_tags = ['dd', 'dt', 'li', 'option', 'optgroup', 'p', 'rp', 'rt'];
/**
* @param {string} tag
* @param {string} parent_tag
* @returns {boolean}
*/
function is_tag_valid_with_parent(tag, parent_tag) {
// First, let's check if we're in an unusual parsing mode...
switch (parent_tag) {
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
case 'select':
return tag === 'option' || tag === 'optgroup' || tag === '#text';
case 'optgroup':
return tag === 'option' || tag === '#text';
// Strictly speaking, seeing an <option> doesn't mean we're in a <select>
// but
case 'option':
return tag === '#text';
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption
// No special behavior since these rules fall back to "in body" mode for
// all except special table nodes which cause bad parsing behavior anyway.
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intr
case 'tr':
return (
tag === 'th' || tag === 'td' || tag === 'style' || tag === 'script' || tag === 'template'
);
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intbody
case 'tbody':
case 'thead':
case 'tfoot':
return tag === 'tr' || tag === 'style' || tag === 'script' || tag === 'template';
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incolgroup
case 'colgroup':
return tag === 'col' || tag === 'template';
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intable
case 'table':
return (
tag === 'caption' ||
tag === 'colgroup' ||
tag === 'tbody' ||
tag === 'tfoot' ||
tag === 'thead' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inhead
case 'head':
return (
tag === 'base' ||
tag === 'basefont' ||
tag === 'bgsound' ||
tag === 'link' ||
tag === 'meta' ||
tag === 'title' ||
tag === 'noscript' ||
tag === 'noframes' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
// https://html.spec.whatwg.org/multipage/semantics.html#the-html-element
case 'html':
return tag === 'head' || tag === 'body' || tag === 'frameset';
case 'frameset':
return tag === 'frame';
case '#document':
return tag === 'html';
}
// Probably in the "in body" parsing mode, so we outlaw only tag combos
// where the parsing rules cause implicit opens or closes to be added.
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
switch (tag) {
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
return (
parent_tag !== 'h1' &&
parent_tag !== 'h2' &&
parent_tag !== 'h3' &&
parent_tag !== 'h4' &&
parent_tag !== 'h5' &&
parent_tag !== 'h6'
);
case 'rp':
case 'rt':
return implied_end_tags.indexOf(parent_tag) === -1;
case 'body':
case 'caption':
case 'col':
case 'colgroup':
case 'frameset':
case 'frame':
case 'head':
case 'html':
case 'tbody':
case 'td':
case 'tfoot':
case 'th':
case 'thead':
case 'tr':
// These tags are only valid with a few parents that have special child
// parsing rules -- if we're down here, then none of those matched and
// so we allow it only if we don't know what the parent is, as all other
// cases are invalid.
return parent_tag == null;
}
return true;
}
/**
* @type {import('../types').Visitors}
*/
export const validate_template = {
Attribute(node) {
if (node.name.startsWith('on') && node.name.length > 2) {
if (node.value === true || is_text_attribute(node) || node.value.length > 1) {
error(node, 'invalid-event-attribute-value');
}
}
},
BindDirective(node, context) {
validate_no_const_assignment(node, node.expression, context.state.scope, true);
let left = node.expression;
while (left.type === 'MemberExpression') {
left = /** @type {import('estree').MemberExpression} */ (left.object);
}
if (left.type !== 'Identifier') {
error(node, 'invalid-binding-expression');
}
if (
node.expression.type === 'Identifier' &&
node.name !== 'this' // bind:this also works for regular variables
) {
const binding = context.state.scope.get(left.name);
// reassignment
if (
!binding ||
(binding.kind !== 'state' &&
binding.kind !== 'prop' &&
binding.kind !== 'each' &&
binding.kind !== 'store_sub' &&
!binding.mutated)
) {
error(node.expression, 'invalid-binding-value');
}
// TODO handle mutations of non-state/props in runes mode
}
if (node.name === 'group') {
const binding = context.state.scope.get(left.name);
if (!binding) {
error(node, 'INTERNAL', 'Cannot find declaration for bind:group');
}
}
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') {
error(
node,
'invalid-binding',
node.name,
undefined,
'. 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)) {
error(
node,
'invalid-binding',
node.name,
property.valid_elements.map((valid_element) => `<${valid_element}>`).join(', ')
);
}
if (parent.name === 'input') {
const type = /** @type {import('#compiler').Attribute | undefined} */ (
parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'type')
);
if (type && !is_text_attribute(type)) {
error(type, 'invalid-type-attribute');
}
if (node.name === 'checked' && type?.value[0].data !== 'checkbox') {
error(node, 'invalid-binding', node.name, '<input type="checkbox">');
}
if (node.name === 'files' && type?.value[0].data !== 'file') {
error(node, 'invalid-binding', node.name, '<input type="file">');
}
}
if (parent.name === 'select') {
const multiple = parent.attributes.find(
(a) =>
a.type === 'Attribute' &&
a.name === 'multiple' &&
!is_text_attribute(a) &&
a.value !== true
);
if (multiple) {
error(multiple, 'invalid-multiple-attribute');
}
}
if (node.name === 'offsetWidth' && SVGElements.includes(parent.name)) {
error(
node,
'invalid-binding',
node.name,
`non-<svg> elements. Use 'clientWidth' for <svg> instead`
);
}
} 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)) {
error(node, 'invalid-binding', node.name, undefined, ` (did you mean '${match}'?)`);
}
}
error(node, 'invalid-binding', node.name);
}
}
},
RegularElement(node, context) {
if (node.name === 'textarea' && node.fragment.nodes.length > 0) {
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute' && attribute.name === 'value') {
error(node, 'invalid-textarea-content');
}
}
}
validate_element(node, context);
if (context.state.parent_element) {
if (!is_tag_valid_with_parent(node.name, context.state.parent_element)) {
error(node, 'invalid-node-placement', `<${node.name}>`, context.state.parent_element);
}
}
context.next({
...context.state,
parent_element: node.name
});
},
SvelteElement(node, context) {
validate_element(node, context);
context.next({
...context.state,
parent_element: null
});
},
SvelteFragment(node, context) {
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
if (attribute.name === 'slot') {
validate_slot_attribute(context, attribute);
}
} else if (attribute.type !== 'LetDirective') {
error(attribute, 'invalid-svelte-fragment-attribute');
}
}
},
SlotElement(node) {
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
if (attribute.name === 'name') {
if (!is_text_attribute(attribute)) {
error(attribute, 'invalid-slot-name');
}
}
} else if (attribute.type !== 'SpreadAttribute') {
error(attribute, 'invalid-slot-element-attribute');
}
}
},
Component: validate_component,
SvelteComponent: validate_component,
SvelteSelf: validate_component,
Text(node, context) {
if (!node.parent) return;
if (context.state.parent_element && regex_not_whitespace.test(node.data)) {
if (!is_tag_valid_with_parent('#text', context.state.parent_element)) {
error(node, 'invalid-node-placement', 'Text node', context.state.parent_element);
}
}
},
ExpressionTag(node, context) {
if (!node.parent) return;
if (context.state.parent_element) {
if (!is_tag_valid_with_parent('#text', context.state.parent_element)) {
error(node, 'invalid-node-placement', '{expression}', context.state.parent_element);
}
}
}
};

@ -1,639 +1,9 @@
import { error } from '../../../errors.js';
import { extract_identifiers, is_text_attribute } from '../../../utils/ast.js';
import { warn } from '../../../warnings.js';
import fuzzymatch from '../../1-parse/utils/fuzzymatch.js';
import { binding_properties } from '../../bindings.js';
import { SVGElements } from '../../constants.js';
import { is_custom_element_node } from '../../nodes.js';
import { regex_not_whitespace, regex_only_whitespaces } from '../../patterns.js';
import { Scope, get_rune } from '../../scope.js';
import { merge } from '../../visitors.js'; import { merge } from '../../visitors.js';
import { validate_assignment, validate_no_const_assignment } from '../utils.js';
import { validate_a11y } from './validate-a11y.js'; import { validate_a11y } from './validate-a11y.js';
import { validate_legacy } from './validate-legacy.js'; import { validate_legacy } from './validate-legacy.js';
import { validate_runes } from './validate-runes.js';
import { validate_template } from './validate-template.js';
/** export const validation_legacy = merge(validate_template, validate_a11y, validate_legacy);
* @param {import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf} node
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('../types.js').AnalysisState>} context
*/
function validate_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'
) {
error(attribute, 'invalid-component-directive');
}
}
context.next({ export const validation_runes = merge(validate_template, validate_a11y, validate_runes);
...context.state,
parent_element: null,
component_slots: new Set()
});
}
/**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('../types.js').AnalysisState>} context
*/
function validate_element(node, context) {
for (const attribute of node.attributes) {
if (
attribute.type === 'Attribute' &&
attribute.name === 'is' &&
context.state.options.namespace !== 'foreign'
) {
warn(context.state.analysis.warnings, attribute, context.path, 'avoid-is');
}
if (attribute.type === 'Attribute' && attribute.name === 'slot') {
/** @type {import('#compiler').RegularElement | import('#compiler').SvelteElement | import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf | undefined} */
validate_slot_attribute(context, attribute);
}
}
}
/**
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('../types.js').AnalysisState>} context
* @param {import('#compiler').Attribute} attribute
*/
function validate_slot_attribute(context, attribute) {
let owner = undefined;
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)) {
error(attribute, 'invalid-slot-attribute');
}
if (owner.type === 'Component' || owner.type === 'SvelteComponent') {
if (owner !== context.path.at(-2)) {
error(attribute, 'invalid-slot-placement');
}
}
const name = attribute.value[0].data;
if (context.state.component_slots.has(name)) {
error(attribute, 'duplicate-slot-name', 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;
}
}
error(node, 'invalid-default-slot-content');
}
}
} else {
error(attribute, 'invalid-slot-placement');
}
}
// https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags
const implied_end_tags = ['dd', 'dt', 'li', 'option', 'optgroup', 'p', 'rp', 'rt'];
/**
* @param {string} tag
* @param {string} parent_tag
* @returns {boolean}
*/
function is_tag_valid_with_parent(tag, parent_tag) {
// First, let's check if we're in an unusual parsing mode...
switch (parent_tag) {
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
case 'select':
return tag === 'option' || tag === 'optgroup' || tag === '#text';
case 'optgroup':
return tag === 'option' || tag === '#text';
// Strictly speaking, seeing an <option> doesn't mean we're in a <select>
// but
case 'option':
return tag === '#text';
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption
// No special behavior since these rules fall back to "in body" mode for
// all except special table nodes which cause bad parsing behavior anyway.
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intr
case 'tr':
return (
tag === 'th' || tag === 'td' || tag === 'style' || tag === 'script' || tag === 'template'
);
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intbody
case 'tbody':
case 'thead':
case 'tfoot':
return tag === 'tr' || tag === 'style' || tag === 'script' || tag === 'template';
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incolgroup
case 'colgroup':
return tag === 'col' || tag === 'template';
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intable
case 'table':
return (
tag === 'caption' ||
tag === 'colgroup' ||
tag === 'tbody' ||
tag === 'tfoot' ||
tag === 'thead' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inhead
case 'head':
return (
tag === 'base' ||
tag === 'basefont' ||
tag === 'bgsound' ||
tag === 'link' ||
tag === 'meta' ||
tag === 'title' ||
tag === 'noscript' ||
tag === 'noframes' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
// https://html.spec.whatwg.org/multipage/semantics.html#the-html-element
case 'html':
return tag === 'head' || tag === 'body' || tag === 'frameset';
case 'frameset':
return tag === 'frame';
case '#document':
return tag === 'html';
}
// Probably in the "in body" parsing mode, so we outlaw only tag combos
// where the parsing rules cause implicit opens or closes to be added.
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
switch (tag) {
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
return (
parent_tag !== 'h1' &&
parent_tag !== 'h2' &&
parent_tag !== 'h3' &&
parent_tag !== 'h4' &&
parent_tag !== 'h5' &&
parent_tag !== 'h6'
);
case 'rp':
case 'rt':
return implied_end_tags.indexOf(parent_tag) === -1;
case 'body':
case 'caption':
case 'col':
case 'colgroup':
case 'frameset':
case 'frame':
case 'head':
case 'html':
case 'tbody':
case 'td':
case 'tfoot':
case 'th':
case 'thead':
case 'tr':
// These tags are only valid with a few parents that have special child
// parsing rules -- if we're down here, then none of those matched and
// so we allow it only if we don't know what the parent is, as all other
// cases are invalid.
return parent_tag == null;
}
return true;
}
/**
* @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, import('../types.js').AnalysisState>}
*/
export const validation = {
Attribute(node) {
if (node.name.startsWith('on') && node.name.length > 2) {
if (node.value === true || is_text_attribute(node) || node.value.length > 1) {
error(node, 'invalid-event-attribute-value');
}
}
},
BindDirective(node, context) {
validate_no_const_assignment(node, node.expression, context.state.scope, true);
let left = node.expression;
while (left.type === 'MemberExpression') {
left = /** @type {import('estree').MemberExpression} */ (left.object);
}
if (left.type !== 'Identifier') {
error(node, 'invalid-binding-expression');
}
if (
node.expression.type === 'Identifier' &&
node.name !== 'this' // bind:this also works for regular variables
) {
const binding = context.state.scope.get(left.name);
// reassignment
if (
!binding ||
(binding.kind !== 'state' &&
binding.kind !== 'prop' &&
binding.kind !== 'each' &&
binding.kind !== 'store_sub' &&
!binding.mutated)
) {
error(node.expression, 'invalid-binding-value');
}
// TODO handle mutations of non-state/props in runes mode
}
if (node.name === 'group') {
const binding = context.state.scope.get(left.name);
if (!binding) {
error(node, 'INTERNAL', 'Cannot find declaration for bind:group');
}
}
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') {
error(
node,
'invalid-binding',
node.name,
undefined,
'. 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)) {
error(
node,
'invalid-binding',
node.name,
property.valid_elements.map((valid_element) => `<${valid_element}>`).join(', ')
);
}
if (parent.name === 'input') {
const type = /** @type {import('#compiler').Attribute | undefined} */ (
parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'type')
);
if (type && !is_text_attribute(type)) {
error(type, 'invalid-type-attribute');
}
if (node.name === 'checked' && type?.value[0].data !== 'checkbox') {
error(node, 'invalid-binding', node.name, '<input type="checkbox">');
}
if (node.name === 'files' && type?.value[0].data !== 'file') {
error(node, 'invalid-binding', node.name, '<input type="file">');
}
}
if (parent.name === 'select') {
const multiple = parent.attributes.find(
(a) =>
a.type === 'Attribute' &&
a.name === 'multiple' &&
!is_text_attribute(a) &&
a.value !== true
);
if (multiple) {
error(multiple, 'invalid-multiple-attribute');
}
}
if (node.name === 'offsetWidth' && SVGElements.includes(parent.name)) {
error(
node,
'invalid-binding',
node.name,
`non-<svg> elements. Use 'clientWidth' for <svg> instead`
);
}
} 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)) {
error(node, 'invalid-binding', node.name, undefined, ` (did you mean '${match}'?)`);
}
}
error(node, 'invalid-binding', node.name);
}
}
},
RegularElement(node, context) {
if (node.name === 'textarea' && node.fragment.nodes.length > 0) {
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute' && attribute.name === 'value') {
error(node, 'invalid-textarea-content');
}
}
}
validate_element(node, context);
if (context.state.parent_element) {
if (!is_tag_valid_with_parent(node.name, context.state.parent_element)) {
error(node, 'invalid-node-placement', `<${node.name}>`, context.state.parent_element);
}
}
context.next({
...context.state,
parent_element: node.name
});
},
SvelteElement(node, context) {
validate_element(node, context);
context.next({
...context.state,
parent_element: null
});
},
SvelteFragment(node, context) {
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
if (attribute.name === 'slot') {
validate_slot_attribute(context, attribute);
}
} else if (attribute.type !== 'LetDirective') {
error(attribute, 'invalid-svelte-fragment-attribute');
}
}
},
SlotElement(node) {
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
if (attribute.name === 'name') {
if (!is_text_attribute(attribute)) {
error(attribute, 'invalid-slot-name');
}
}
} else if (attribute.type !== 'SpreadAttribute') {
error(attribute, 'invalid-slot-element-attribute');
}
}
},
Component: validate_component,
SvelteComponent: validate_component,
SvelteSelf: validate_component,
Text(node, context) {
if (!node.parent) return;
if (context.state.parent_element && regex_not_whitespace.test(node.data)) {
if (!is_tag_valid_with_parent('#text', context.state.parent_element)) {
error(node, 'invalid-node-placement', 'Text node', context.state.parent_element);
}
}
},
ExpressionTag(node, context) {
if (!node.parent) return;
if (context.state.parent_element) {
if (!is_tag_valid_with_parent('#text', context.state.parent_element)) {
error(node, 'invalid-node-placement', '{expression}', context.state.parent_element);
}
}
}
};
export const validation_legacy = merge(validation, validate_a11y, validate_legacy);
/**
*
* @param {import('estree').Node} node
* @param {import('../../scope.js').Scope} scope
* @param {string} name
*/
function validate_export(node, scope, name) {
const binding = scope.get(name);
if (binding && (binding.kind === 'derived' || binding.kind === 'state')) {
error(node, 'invalid-rune-export', `$${binding.kind}`);
}
}
/**
* @param {import('estree').CallExpression} node
* @param {Scope} scope
* @param {import('#compiler').SvelteNode[]} path
* @returns
*/
function validate_call_expression(node, scope, path) {
const rune = get_rune(node, scope);
if (rune === null) return;
if (rune === '$props' && path.at(-1)?.type !== 'VariableDeclarator') {
error(node, 'invalid-props-location');
} else if (
(rune === '$state' || rune === '$derived') &&
path.at(-1)?.type !== 'VariableDeclarator' &&
path.at(-1)?.type !== 'PropertyDefinition'
) {
error(node, rune === '$derived' ? 'invalid-derived-location' : 'invalid-state-location');
} else if (rune === '$effect') {
if (path.at(-1)?.type !== 'ExpressionStatement') {
error(node, 'invalid-effect-location');
} else if (node.arguments.length !== 1) {
error(node, 'invalid-rune-args-length', '$effect', [1]);
}
}
}
/**
* @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, import('../types.js').AnalysisState>}
*/
export const validation_runes_js = {
ExportSpecifier(node, { state }) {
validate_export(node, state.scope, node.local.name);
},
ExportNamedDeclaration(node, { state, next }) {
if (node.declaration?.type !== 'VariableDeclaration') return;
// visit children, so bindings are correctly initialised
next();
for (const declarator of node.declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) {
validate_export(node, state.scope, id.name);
}
}
},
CallExpression(node, { state, path }) {
validate_call_expression(node, state.scope, path);
},
VariableDeclarator(node, { state }) {
const init = node.init;
const rune = get_rune(init, state.scope);
if (rune === null) return;
const args = /** @type {import('estree').CallExpression} */ (init).arguments;
if (rune === '$derived' && args.length !== 1) {
error(node, 'invalid-rune-args-length', '$derived', [1]);
} else if (rune === '$state' && args.length > 1) {
error(node, 'invalid-rune-args-length', '$state', [0, 1]);
} else if (rune === '$props') {
error(node, 'invalid-props-location');
}
},
AssignmentExpression(node, { state }) {
validate_assignment(node, node.left, state);
},
UpdateExpression(node, { state }) {
validate_assignment(node, node.argument, state);
},
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') {
private_derived_state.push(definition.key.name);
}
}
}
context.next({
...context.state,
private_derived_state
});
}
};
export const validation_runes = merge(validation, validate_a11y, {
AssignmentExpression(node, { state, path }) {
const parent = path.at(-1);
if (parent && parent.type === 'ConstTag') return;
validate_assignment(node, node.left, state);
},
UpdateExpression(node, { state }) {
validate_assignment(node, node.argument, state);
},
LabeledStatement(node, { path }) {
if (node.label.name !== '$' || path.at(-1)?.type !== 'Program') return;
error(node, 'invalid-legacy-reactive-statement');
},
ExportNamedDeclaration(node, { state }) {
if (node.declaration?.type !== 'VariableDeclaration') return;
if (node.declaration.kind !== 'let') return;
if (state.analysis.instance.scope !== state.scope) return;
error(node, 'invalid-legacy-export');
},
ExportSpecifier(node, { state }) {
validate_export(node, state.scope, node.local.name);
},
CallExpression(node, { state, path }) {
validate_call_expression(node, state.scope, path);
},
EachBlock(node, { next, state }) {
const context = node.context;
if (
context.type === 'Identifier' &&
(context.name === '$state' || context.name === '$derived')
) {
error(
node,
context.name === '$derived' ? 'invalid-derived-location' : 'invalid-state-location'
);
}
next({ ...state });
},
VariableDeclarator(node, { state }) {
const init = node.init;
const rune = get_rune(init, state.scope);
if (rune === null) return;
const args = /** @type {import('estree').CallExpression} */ (init).arguments;
if (rune === '$derived' && args.length !== 1) {
error(node, 'invalid-rune-args-length', '$derived', [1]);
} else if (rune === '$state' && args.length > 1) {
error(node, 'invalid-rune-args-length', '$state', [0, 1]);
} else if (rune === '$props') {
if (state.has_props_rune) {
error(node, 'duplicate-props-rune');
}
state.has_props_rune = true;
if (args.length > 0) {
error(node, 'invalid-rune-args-length', '$props', [0]);
}
if (node.id.type !== 'ObjectPattern') {
error(node, 'invalid-props-id');
}
if (state.scope !== state.analysis.instance.scope) {
error(node, 'invalid-props-location');
}
for (const property of node.id.properties) {
if (property.type === 'Property') {
if (property.computed) {
error(property, 'invalid-props-pattern');
}
const value =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
if (value.type !== 'Identifier') {
error(property, 'invalid-props-pattern');
}
}
}
}
},
ClassBody: validation_runes_js.ClassBody
});

Loading…
Cancel
Save