diff --git a/src/validate/html/index.ts b/src/validate/html/index.ts
index b09cece0e4..ad653daa63 100644
--- a/src/validate/html/index.ts
+++ b/src/validate/html/index.ts
@@ -1,6 +1,8 @@
import * as namespaces from '../../utils/namespaces';
import validateElement from './validateElement';
import validateWindow from './validateWindow';
+import fuzzymatch from '../utils/fuzzymatch'
+import flattenReference from '../../utils/flattenReference';
import { Validator } from '../index';
import { Node } from '../../interfaces';
@@ -11,6 +13,9 @@ const meta = new Map([[':Window', validateWindow]]);
export default function validateHtml(validator: Validator, html: Node) {
let elementDepth = 0;
+ const refs = new Map();
+ const refCallees: Node[] = [];
+
function visit(node: Node) {
if (node.type === 'Element') {
if (
@@ -25,12 +30,12 @@ export default function validateHtml(validator: Validator, html: Node) {
}
if (meta.has(node.name)) {
- return meta.get(node.name)(validator, node);
+ return meta.get(node.name)(validator, node, refs, refCallees);
}
elementDepth += 1;
- validateElement(validator, node);
+ validateElement(validator, node, refs, refCallees);
} else if (node.type === 'EachBlock') {
if (validator.helpers.has(node.context)) {
let c = node.expression.end;
@@ -61,4 +66,20 @@ export default function validateHtml(validator: Validator, html: Node) {
}
html.children.forEach(visit);
+
+ refCallees.forEach(callee => {
+ const { parts } = flattenReference(callee);
+ const ref = parts[1];
+
+ if (refs.has(ref)) {
+ // TODO check method is valid, e.g. `audio.stop()` should be `audio.pause()`
+ } else {
+ const match = fuzzymatch(ref, Array.from(refs.keys()));
+
+ let message = `'refs.${ref}' does not exist`;
+ if (match) message += ` (did you mean 'refs.${match}'?)`;
+
+ validator.error(message, callee.start);
+ }
+ });
}
diff --git a/src/validate/html/validateElement.ts b/src/validate/html/validateElement.ts
index 69a50b6cff..6658895c58 100644
--- a/src/validate/html/validateElement.ts
+++ b/src/validate/html/validateElement.ts
@@ -2,7 +2,7 @@ import validateEventHandler from './validateEventHandler';
import { Validator } from '../index';
import { Node } from '../../interfaces';
-export default function validateElement(validator: Validator, node: Node) {
+export default function validateElement(validator: Validator, node: Node, refs: Map, refCallees: Node[]) {
const isComponent =
node.name === ':Self' || validator.components.has(node.name);
@@ -16,6 +16,11 @@ export default function validateElement(validator: Validator, node: Node) {
let hasTransition: boolean;
node.attributes.forEach((attribute: Node) => {
+ if (attribute.type === 'Ref') {
+ if (!refs.has(attribute.name)) refs.set(attribute.name, []);
+ refs.get(attribute.name).push(node);
+ }
+
if (!isComponent && attribute.type === 'Binding') {
const { name } = attribute;
@@ -80,7 +85,7 @@ export default function validateElement(validator: Validator, node: Node) {
);
}
} else if (attribute.type === 'EventHandler') {
- validateEventHandler(validator, attribute);
+ validateEventHandler(validator, attribute, refCallees);
} else if (attribute.type === 'Transition') {
const bidi = attribute.intro && attribute.outro;
diff --git a/src/validate/html/validateEventHandler.ts b/src/validate/html/validateEventHandler.ts
index 85fdad08e6..71d8a26fc5 100644
--- a/src/validate/html/validateEventHandler.ts
+++ b/src/validate/html/validateEventHandler.ts
@@ -7,7 +7,8 @@ const validBuiltins = new Set(['set', 'fire', 'destroy']);
export default function validateEventHandlerCallee(
validator: Validator,
- attribute: Node
+ attribute: Node,
+ refCallees: Node[]
) {
const { callee, start, type } = attribute.expression;
@@ -18,6 +19,12 @@ export default function validateEventHandlerCallee(
const { name } = flattenReference(callee);
if (name === 'this' || name === 'event') return;
+
+ if (name === 'refs') {
+ refCallees.push(callee);
+ return;
+ }
+
if (
(callee.type === 'Identifier' && validBuiltins.has(callee.name)) ||
validator.methods.has(callee.name)
diff --git a/src/validate/html/validateWindow.ts b/src/validate/html/validateWindow.ts
index 7c78b7f37d..e451bcc534 100644
--- a/src/validate/html/validateWindow.ts
+++ b/src/validate/html/validateWindow.ts
@@ -14,7 +14,7 @@ const validBindings = [
'scrollY',
];
-export default function validateWindow(validator: Validator, node: Node) {
+export default function validateWindow(validator: Validator, node: Node, refs: Map, refCallees: Node[]) {
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Binding') {
if (attribute.value.type !== 'Identifier') {
@@ -50,7 +50,7 @@ export default function validateWindow(validator: Validator, node: Node) {
}
}
} else if (attribute.type === 'EventHandler') {
- validateEventHandler(validator, attribute);
+ validateEventHandler(validator, attribute, refCallees);
}
});
}
diff --git a/test/validator/index.js b/test/validator/index.js
index 0782997573..9b4f4900b7 100644
--- a/test/validator/index.js
+++ b/test/validator/index.js
@@ -17,6 +17,10 @@ describe("validate", () => {
const filename = `test/validator/samples/${dir}/input.html`;
const input = fs.readFileSync(filename, "utf-8").replace(/\s+$/, "");
+ const expectedWarnings = tryToLoadJson(`test/validator/samples/${dir}/warnings.json`) || [];
+ const expectedErrors = tryToLoadJson(`test/validator/samples/${dir}/errors.json`);
+ let error;
+
try {
const warnings = [];
@@ -30,20 +34,25 @@ describe("validate", () => {
}
});
- const expectedWarnings =
- tryToLoadJson(`test/validator/samples/${dir}/warnings.json`) || [];
-
assert.deepEqual(warnings, expectedWarnings);
- } catch (err) {
- try {
- const expected = require(`./samples/${dir}/errors.json`)[0];
+ } catch (e) {
+ error = e;
+ }
+
+ const expected = expectedErrors && expectedErrors[0];
- assert.equal(err.message, expected.message);
- assert.deepEqual(err.loc, expected.loc);
- assert.equal(err.pos, expected.pos);
- } catch (err2) {
- throw err2.code === "MODULE_NOT_FOUND" ? err : err2;
+ if (error || expected) {
+ if (error && !expected) {
+ throw error;
}
+
+ if (expected && !error) {
+ throw new Error(`Expected an error: ${expected.message}`);
+ }
+
+ assert.equal(error.message, expected.message);
+ assert.deepEqual(error.loc, expected.loc);
+ assert.equal(error.pos, expected.pos);
}
});
});
diff --git a/test/validator/samples/event-handler-ref-invalid/errors.json b/test/validator/samples/event-handler-ref-invalid/errors.json
new file mode 100644
index 0000000000..3ec1eb61b6
--- /dev/null
+++ b/test/validator/samples/event-handler-ref-invalid/errors.json
@@ -0,0 +1,8 @@
+[{
+ "message": "'refs.inputx' does not exist (did you mean 'refs.input'?)",
+ "pos": 36,
+ "loc": {
+ "line": 2,
+ "column": 18
+ }
+}]
\ No newline at end of file
diff --git a/test/validator/samples/event-handler-ref-invalid/input.html b/test/validator/samples/event-handler-ref-invalid/input.html
new file mode 100644
index 0000000000..f6cbe90c3a
--- /dev/null
+++ b/test/validator/samples/event-handler-ref-invalid/input.html
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/test/validator/samples/event-handler-ref/input.html b/test/validator/samples/event-handler-ref/input.html
new file mode 100644
index 0000000000..e4fa0dec0f
--- /dev/null
+++ b/test/validator/samples/event-handler-ref/input.html
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/test/validator/samples/event-handler-ref/warnings.json b/test/validator/samples/event-handler-ref/warnings.json
new file mode 100644
index 0000000000..0637a088a0
--- /dev/null
+++ b/test/validator/samples/event-handler-ref/warnings.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file