svelte
Version:
Cybernetically enhanced web apps
255 lines (217 loc) • 8.06 kB
JavaScript
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import {
extract_all_identifiers_from_expression,
is_text_attribute,
object
} from '../../../utils/ast.js';
import { validate_assignment } from './shared/utils.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { binding_properties } from '../../bindings.js';
import fuzzymatch from '../../1-parse/utils/fuzzymatch.js';
import { is_content_editable_binding, is_svg } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.BindDirective} node
* @param {Context} context
*/
export function BindDirective(node, context) {
const parent = context.path.at(-1);
if (
parent?.type === 'RegularElement' ||
parent?.type === 'SvelteElement' ||
parent?.type === 'SvelteWindow' ||
parent?.type === 'SvelteDocument' ||
parent?.type === 'SvelteBody'
) {
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 {AST.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' && is_svg(parent.name)) {
e.bind_invalid_target(
node,
node.name,
`non-<svg> elements. Use 'clientWidth' for <svg> instead`
);
}
if (is_content_editable_binding(node.name)) {
const contenteditable = /** @type {AST.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);
}
}
// When dealing with bind getters/setters skip the specific binding validation
// Group bindings aren't supported for getter/setters so we don't need to handle
// the metadata
if (node.expression.type === 'SequenceExpression') {
if (node.name === 'group') {
e.bind_group_invalid_expression(node);
}
let i = /** @type {number} */ (node.expression.start);
let leading_comments_start = /**@type {any}*/ (node.expression.leadingComments?.at(0))?.start;
let leading_comments_end = /**@type {any}*/ (node.expression.leadingComments?.at(-1))?.end;
while (context.state.analysis.source[--i] !== '{') {
if (
context.state.analysis.source[i] === '(' &&
// if the parenthesis is in a leading comment we don't need to throw the error
!(
leading_comments_start &&
leading_comments_end &&
i <= leading_comments_end &&
i >= leading_comments_start
)
) {
e.bind_invalid_parens(node, node.name);
}
}
if (node.expression.expressions.length !== 2) {
e.bind_invalid_expression(node);
}
mark_subtree_dynamic(context.path);
return;
}
validate_assignment(node, node.expression, context.state);
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 !== 'raw_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'each' &&
binding.kind !== 'store_sub' &&
!binding.updated)) // TODO wut?
) {
e.bind_invalid_value(node.expression);
}
}
if (node.name === 'group') {
if (!binding) {
throw new Error('Cannot find declaration for bind:group');
}
if (binding.kind === 'snippet') {
e.bind_group_invalid_snippet_parameter(node);
}
// 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;
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);
}
context.next();
}