Skip to content

Commit 14a5260

Browse files
committed
feat: implement message overloads
1 parent 476f217 commit 14a5260

File tree

19 files changed

+163
-187
lines changed

19 files changed

+163
-187
lines changed

packages/svelte/messages/client-warnings/warnings.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,10 @@
44
55
## ownership_invalid_binding
66

7-
> %parent% passed a value to %child% with `bind:`, but the value is owned by %owner%. Consider creating a binding between %owner% and %parent%
7+
> %parent% passed a value to %child% with `bind:`, but the value is owned by %owner%. Consider creating a binding between %owner% and %parent%
8+
9+
## ownership_invalid_mutation
10+
11+
> Mutating a value outside the component that created it is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
12+
13+
> %component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead

packages/svelte/messages/compile-errors/bindings.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414

1515
> `bind:%name%` is not a valid binding
1616
17-
## bind_invalid_detailed
18-
1917
> `bind:%name%` is not a valid binding. %explanation%
2018
2119
## invalid_type_attribute
@@ -32,4 +30,4 @@
3230
3331
## dynamic_contenteditable_attribute
3432

35-
> 'contenteditable' attribute cannot be dynamic if element uses two-way binding
33+
> 'contenteditable' attribute cannot be dynamic if element uses two-way binding

packages/svelte/messages/compile-warnings/a11y.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66

77
> Unknown aria attribute 'aria-%attribute%'
88
9-
## a11y_unknown_aria_attribute_suggestion
10-
119
> Unknown aria attribute 'aria-%attribute%'. Did you mean '%suggestion%'?
1210
1311
## a11y_hidden
@@ -62,8 +60,6 @@
6260

6361
> Unknown role '%role%'
6462
65-
## a11y_unknown_role_suggestion
66-
6763
> Unknown role '%role%'. Did you mean '%suggestion%'?
6864
6965
## a11y_no_redundant_roles
@@ -168,4 +164,4 @@
168164
169165
## a11y_missing_content
170166

171-
> <%name%> element should have child content
167+
> <%name%> element should have child content

packages/svelte/messages/compile-warnings/options.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## options_deprecated_accessors
22

3-
The `accessors` option has been deprecated. It will have no effect in runes mode
3+
> The `accessors` option has been deprecated. It will have no effect in runes mode
44
55
## options_deprecated_immutable
66

@@ -24,4 +24,4 @@ The `accessors` option has been deprecated. It will have no effect in runes mode
2424
2525
## options_removed_loop_guard_timeout
2626

27-
> The `loopGuardTimeout` option has been removed
27+
> The `loopGuardTimeout` option has been removed

packages/svelte/scripts/process-messages/index.js

Lines changed: 107 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as acorn from 'acorn';
44
import { walk } from 'zimmerframe';
55
import * as esrap from 'esrap';
66

7+
/** @type {Record<string, Record<string, { messages: string[], details: string | null }>>} */
78
const messages = {};
89
const seen = new Set();
910

@@ -24,12 +25,21 @@ for (const category of fs.readdirSync('messages')) {
2425
throw new Error(`Duplicate message code ${category}/${code}`);
2526
}
2627

28+
const sections = text.trim().split('\n\n');
29+
let details = null;
30+
if (!sections[sections.length - 1].startsWith('> ')) {
31+
details = /** @type {string} */ (sections.pop());
32+
}
33+
34+
if (sections.length === 0) {
35+
throw new Error('No message text');
36+
}
37+
2738
seen.add(code);
28-
messages[category][code] = text
29-
.trim()
30-
.split('\n')
31-
.map((line) => line.slice(2))
32-
.join('\n');
39+
messages[category][code] = {
40+
messages: sections.map((section) => section.replace(/^> /gm, '')),
41+
details
42+
};
3343
}
3444
}
3545
}
@@ -102,13 +112,89 @@ function transform(name, dest) {
102112
ast.body.splice(index, 1);
103113

104114
for (const code in category) {
105-
const message = category[code];
115+
const { messages } = category[code];
106116
const vars = [];
107-
for (const match of message.matchAll(/%(\w+)%/g)) {
108-
const name = match[1];
109-
if (!vars.includes(name)) {
110-
vars.push(match[1]);
117+
118+
const group = messages.map((text, i) => {
119+
for (const match of text.matchAll(/%(\w+)%/g)) {
120+
const name = match[1];
121+
if (!vars.includes(name)) {
122+
vars.push(match[1]);
123+
}
124+
}
125+
126+
return {
127+
text,
128+
vars: vars.slice()
129+
};
130+
});
131+
132+
/** @type {import('estree').Expression} */
133+
let message = { type: 'Literal', value: '' };
134+
let prev_vars;
135+
136+
for (let i = 0; i < group.length; i += 1) {
137+
const { text, vars } = group[i];
138+
139+
if (vars.length === 0) {
140+
message = {
141+
type: 'Literal',
142+
value: text
143+
};
144+
continue;
111145
}
146+
147+
const parts = text.split(/(%\w+%)/);
148+
149+
/** @type {import('estree').Expression[]} */
150+
const expressions = [];
151+
152+
/** @type {import('estree').TemplateElement[]} */
153+
const quasis = [];
154+
155+
for (let i = 0; i < parts.length; i += 1) {
156+
const part = parts[i];
157+
if (i % 2 === 0) {
158+
const str = part.replace(/(`|\${)/g, '\\$1');
159+
quasis.push({
160+
type: 'TemplateElement',
161+
value: { raw: str, cooked: str },
162+
tail: i === parts.length - 1
163+
});
164+
} else {
165+
expressions.push({
166+
type: 'Identifier',
167+
name: part.slice(1, -1)
168+
});
169+
}
170+
}
171+
172+
/** @type {import('estree').Expression} */
173+
const expression = {
174+
type: 'TemplateLiteral',
175+
expressions,
176+
quasis
177+
};
178+
179+
if (prev_vars) {
180+
if (vars.length === prev_vars.length) {
181+
throw new Error('Message overloads must have new parameters');
182+
}
183+
184+
message = {
185+
type: 'ConditionalExpression',
186+
test: {
187+
type: 'Identifier',
188+
name: vars[prev_vars.length]
189+
},
190+
consequent: expression,
191+
alternate: message
192+
};
193+
} else {
194+
message = expression;
195+
}
196+
197+
prev_vars = vars;
112198
}
113199

114200
const clone = walk(/** @type {import('estree').Node} */ (template_node), null, {
@@ -120,14 +206,22 @@ function transform(name, dest) {
120206
.split('\n')
121207
.map((line) => {
122208
if (line === ' * MESSAGE') {
123-
return message
209+
return messages[messages.length - 1]
124210
.split('\n')
125211
.map((line) => ` * ${line}`)
126212
.join('\n');
127213
}
128214

129215
if (line.includes('PARAMETER')) {
130-
return vars.map((name) => ` * @param {string} ${name}`).join('\n');
216+
return vars
217+
.map((name, i) => {
218+
const optional = i >= group[0].vars.length;
219+
220+
return optional
221+
? ` * @param {string | undefined | null} [${name}]`
222+
: ` * @param {string} ${name}`;
223+
})
224+
.join('\n');
131225
}
132226

133227
return line;
@@ -171,44 +265,7 @@ function transform(name, dest) {
171265
},
172266
Identifier(node) {
173267
if (node.name !== 'MESSAGE') return;
174-
175-
if (/%\w+%/.test(message)) {
176-
const parts = message.split(/(%\w+%)/);
177-
178-
/** @type {import('estree').Expression[]} */
179-
const expressions = [];
180-
181-
/** @type {import('estree').TemplateElement[]} */
182-
const quasis = [];
183-
184-
for (let i = 0; i < parts.length; i += 1) {
185-
const part = parts[i];
186-
if (i % 2 === 0) {
187-
const str = part.replace(/(`|\${)/g, '\\$1');
188-
quasis.push({
189-
type: 'TemplateElement',
190-
value: { raw: str, cooked: str },
191-
tail: i === parts.length - 1
192-
});
193-
} else {
194-
expressions.push({
195-
type: 'Identifier',
196-
name: part.slice(1, -1)
197-
});
198-
}
199-
}
200-
201-
return {
202-
type: 'TemplateLiteral',
203-
expressions,
204-
quasis
205-
};
206-
}
207-
208-
return {
209-
type: 'Literal',
210-
value: message
211-
};
268+
return message;
212269
}
213270
});
214271

packages/svelte/src/compiler/errors.js

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -227,25 +227,15 @@ export function bind_invalid_target(node, name, elements) {
227227
e(node, "bind_invalid_target", `\`bind:${name}\` can only be used with ${elements}`);
228228
}
229229

230-
/**
231-
* `bind:%name%` is not a valid binding
232-
* @param {null | number | NodeLike} node
233-
* @param {string} name
234-
* @returns {never}
235-
*/
236-
export function bind_invalid(node, name) {
237-
e(node, "bind_invalid", `\`bind:${name}\` is not a valid binding`);
238-
}
239-
240230
/**
241231
* `bind:%name%` is not a valid binding. %explanation%
242232
* @param {null | number | NodeLike} node
243233
* @param {string} name
244-
* @param {string} explanation
234+
* @param {string | undefined | null} [explanation]
245235
* @returns {never}
246236
*/
247-
export function bind_invalid_detailed(node, name, explanation) {
248-
e(node, "bind_invalid_detailed", `\`bind:${name}\` is not a valid binding. ${explanation}`);
237+
export function bind_invalid(node, name, explanation) {
238+
e(node, "bind_invalid", explanation ? `\`bind:${name}\` is not a valid binding. ${explanation}` : `\`bind:${name}\` is not a valid binding`);
249239
}
250240

251241
/**

packages/svelte/src/compiler/phases/2-analyze/a11y.js

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -739,12 +739,7 @@ function check_element(node, state) {
739739
const type = name.slice(5);
740740
if (!aria_attributes.includes(type)) {
741741
const match = fuzzymatch(type, aria_attributes);
742-
if (match) {
743-
// TODO allow 'overloads' in messages, so that we can use the same code with and without suggestions
744-
w.a11y_unknown_aria_attribute_suggestion(attribute, type, match);
745-
} else {
746-
w.a11y_unknown_aria_attribute(attribute, type);
747-
}
742+
w.a11y_unknown_aria_attribute(attribute, type, match);
748743
}
749744

750745
if (name === 'aria-hidden' && regex_heading_tags.test(node.name)) {
@@ -792,11 +787,7 @@ function check_element(node, state) {
792787
w.a11y_no_abstract_role(attribute, current_role);
793788
} else if (current_role && !aria_roles.includes(current_role)) {
794789
const match = fuzzymatch(current_role, aria_roles);
795-
if (match) {
796-
w.a11y_unknown_role_suggestion(attribute, current_role, match);
797-
} else {
798-
w.a11y_unknown_role(attribute, current_role);
799-
}
790+
w.a11y_unknown_role(attribute, current_role, match);
800791
}
801792

802793
// no-redundant-roles

packages/svelte/src/compiler/phases/2-analyze/validation.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ const validation = {
373373
parent?.type === 'SvelteBody'
374374
) {
375375
if (context.state.options.namespace === 'foreign' && node.name !== 'this') {
376-
e.bind_invalid_detailed(node, node.name, 'Foreign elements only support `bind:this`');
376+
e.bind_invalid(node, node.name, 'Foreign elements only support `bind:this`');
377377
}
378378

379379
if (node.name in binding_properties) {
@@ -442,7 +442,7 @@ const validation = {
442442
if (match) {
443443
const property = binding_properties[match];
444444
if (!property.valid_elements || property.valid_elements.includes(parent.name)) {
445-
e.bind_invalid_detailed(node, node.name, `Did you mean '${match}'?`);
445+
e.bind_invalid(node, node.name, `Did you mean '${match}'?`);
446446
}
447447
}
448448
e.bind_invalid(node, node.name);

0 commit comments

Comments
 (0)