Skip to content

Commit 6a74db0

Browse files
authored
Merge pull request #731 from sveltejs/gh-686
validate ref callees
2 parents 57e7f75 + c1f34e3 commit 6a74db0

File tree

9 files changed

+73
-18
lines changed

9 files changed

+73
-18
lines changed

src/validate/html/index.ts

+23-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as namespaces from '../../utils/namespaces';
22
import validateElement from './validateElement';
33
import validateWindow from './validateWindow';
4+
import fuzzymatch from '../utils/fuzzymatch'
5+
import flattenReference from '../../utils/flattenReference';
46
import { Validator } from '../index';
57
import { Node } from '../../interfaces';
68

@@ -11,6 +13,9 @@ const meta = new Map([[':Window', validateWindow]]);
1113
export default function validateHtml(validator: Validator, html: Node) {
1214
let elementDepth = 0;
1315

16+
const refs = new Map();
17+
const refCallees: Node[] = [];
18+
1419
function visit(node: Node) {
1520
if (node.type === 'Element') {
1621
if (
@@ -25,12 +30,12 @@ export default function validateHtml(validator: Validator, html: Node) {
2530
}
2631

2732
if (meta.has(node.name)) {
28-
return meta.get(node.name)(validator, node);
33+
return meta.get(node.name)(validator, node, refs, refCallees);
2934
}
3035

3136
elementDepth += 1;
3237

33-
validateElement(validator, node);
38+
validateElement(validator, node, refs, refCallees);
3439
} else if (node.type === 'EachBlock') {
3540
if (validator.helpers.has(node.context)) {
3641
let c = node.expression.end;
@@ -61,4 +66,20 @@ export default function validateHtml(validator: Validator, html: Node) {
6166
}
6267

6368
html.children.forEach(visit);
69+
70+
refCallees.forEach(callee => {
71+
const { parts } = flattenReference(callee);
72+
const ref = parts[1];
73+
74+
if (refs.has(ref)) {
75+
// TODO check method is valid, e.g. `audio.stop()` should be `audio.pause()`
76+
} else {
77+
const match = fuzzymatch(ref, Array.from(refs.keys()));
78+
79+
let message = `'refs.${ref}' does not exist`;
80+
if (match) message += ` (did you mean 'refs.${match}'?)`;
81+
82+
validator.error(message, callee.start);
83+
}
84+
});
6485
}

src/validate/html/validateElement.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import validateEventHandler from './validateEventHandler';
22
import { Validator } from '../index';
33
import { Node } from '../../interfaces';
44

5-
export default function validateElement(validator: Validator, node: Node) {
5+
export default function validateElement(validator: Validator, node: Node, refs: Map<string, Node[]>, refCallees: Node[]) {
66
const isComponent =
77
node.name === ':Self' || validator.components.has(node.name);
88

@@ -16,6 +16,11 @@ export default function validateElement(validator: Validator, node: Node) {
1616
let hasTransition: boolean;
1717

1818
node.attributes.forEach((attribute: Node) => {
19+
if (attribute.type === 'Ref') {
20+
if (!refs.has(attribute.name)) refs.set(attribute.name, []);
21+
refs.get(attribute.name).push(node);
22+
}
23+
1924
if (!isComponent && attribute.type === 'Binding') {
2025
const { name } = attribute;
2126

@@ -80,7 +85,7 @@ export default function validateElement(validator: Validator, node: Node) {
8085
);
8186
}
8287
} else if (attribute.type === 'EventHandler') {
83-
validateEventHandler(validator, attribute);
88+
validateEventHandler(validator, attribute, refCallees);
8489
} else if (attribute.type === 'Transition') {
8590
const bidi = attribute.intro && attribute.outro;
8691

src/validate/html/validateEventHandler.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ const validBuiltins = new Set(['set', 'fire', 'destroy']);
77

88
export default function validateEventHandlerCallee(
99
validator: Validator,
10-
attribute: Node
10+
attribute: Node,
11+
refCallees: Node[]
1112
) {
1213
const { callee, start, type } = attribute.expression;
1314

@@ -18,6 +19,12 @@ export default function validateEventHandlerCallee(
1819
const { name } = flattenReference(callee);
1920

2021
if (name === 'this' || name === 'event') return;
22+
23+
if (name === 'refs') {
24+
refCallees.push(callee);
25+
return;
26+
}
27+
2128
if (
2229
(callee.type === 'Identifier' && validBuiltins.has(callee.name)) ||
2330
validator.methods.has(callee.name)

src/validate/html/validateWindow.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const validBindings = [
1414
'scrollY',
1515
];
1616

17-
export default function validateWindow(validator: Validator, node: Node) {
17+
export default function validateWindow(validator: Validator, node: Node, refs: Map<string, Node[]>, refCallees: Node[]) {
1818
node.attributes.forEach((attribute: Node) => {
1919
if (attribute.type === 'Binding') {
2020
if (attribute.value.type !== 'Identifier') {
@@ -50,7 +50,7 @@ export default function validateWindow(validator: Validator, node: Node) {
5050
}
5151
}
5252
} else if (attribute.type === 'EventHandler') {
53-
validateEventHandler(validator, attribute);
53+
validateEventHandler(validator, attribute, refCallees);
5454
}
5555
});
5656
}

test/validator/index.js

+20-11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ describe("validate", () => {
1717
const filename = `test/validator/samples/${dir}/input.html`;
1818
const input = fs.readFileSync(filename, "utf-8").replace(/\s+$/, "");
1919

20+
const expectedWarnings = tryToLoadJson(`test/validator/samples/${dir}/warnings.json`) || [];
21+
const expectedErrors = tryToLoadJson(`test/validator/samples/${dir}/errors.json`);
22+
let error;
23+
2024
try {
2125
const warnings = [];
2226

@@ -30,20 +34,25 @@ describe("validate", () => {
3034
}
3135
});
3236

33-
const expectedWarnings =
34-
tryToLoadJson(`test/validator/samples/${dir}/warnings.json`) || [];
35-
3637
assert.deepEqual(warnings, expectedWarnings);
37-
} catch (err) {
38-
try {
39-
const expected = require(`./samples/${dir}/errors.json`)[0];
38+
} catch (e) {
39+
error = e;
40+
}
41+
42+
const expected = expectedErrors && expectedErrors[0];
4043

41-
assert.equal(err.message, expected.message);
42-
assert.deepEqual(err.loc, expected.loc);
43-
assert.equal(err.pos, expected.pos);
44-
} catch (err2) {
45-
throw err2.code === "MODULE_NOT_FOUND" ? err : err2;
44+
if (error || expected) {
45+
if (error && !expected) {
46+
throw error;
4647
}
48+
49+
if (expected && !error) {
50+
throw new Error(`Expected an error: ${expected.message}`);
51+
}
52+
53+
assert.equal(error.message, expected.message);
54+
assert.deepEqual(error.loc, expected.loc);
55+
assert.equal(error.pos, expected.pos);
4756
}
4857
});
4958
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[{
2+
"message": "'refs.inputx' does not exist (did you mean 'refs.input'?)",
3+
"pos": 36,
4+
"loc": {
5+
"line": 2,
6+
"column": 18
7+
}
8+
}]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<input ref:input>
2+
<button on:click='refs.inputx.focus()'>focus input</button>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<input ref:input>
2+
<button on:click='refs.input.focus()'>focus input</button>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]

0 commit comments

Comments
 (0)