All files / src/compiler/phases/2-analyze/visitors BindDirective.js

96.59% Statements 170/176
96.84% Branches 92/95
100% Functions 1/1
96.44% Lines 163/169

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 1702x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 995x 995x 995x 995x 995x 995x     989x 989x 989x 995x 717x 717x 717x 544x 542x 542x 542x 542x 542x 542x 544x 717x 2x 2x 715x 717x 1x 1x 714x 717x 2x 2x 712x 717x 1x 1x 717x 983x 995x 128x     128x 983x 995x 5x 5x 983x 983x 983x 983x 995x 995x 995x 995x 298x 995x 685x 1x 1x 684x 685x 681x 681x 3x 3x 3x 3x 3x 3x 678x 681x 2x 2x 94x 94x 82x 82x 94x 2x 2x 2x 2x 2x 2x 2x 2x 2x 676x 681x 380x 380x 380x 380x 3x 1x 1x 2x 2x 377x 380x 1x 1x 376x 380x     380x 672x 681x 68x 68x 82x 82x 82x 6x 68x 68x 1x 1x 68x 671x 681x 1x 1x 1x 1x 1x 1x 670x 680x 22x 22x 22x 22x 1x 22x 1x 1x 22x 685x 3x 3x 1x 1x 1x 1x 1x 2x 2x 685x 995x  
/** @import { Attribute, BindDirective } from '#compiler' */
/** @import { Context } from '../types' */
import { 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');
		}
	}
 
	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);
					}
					return; // bind:value can handle dynamic `type` attributes
				}
 
				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);
		}
	}
}