From c1f34e3e2acbd8c82c541737063ed4fc27cbd6e4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 29 Jul 2017 17:47:10 -0400 Subject: [PATCH] validate ref callees (#686) --- src/validate/html/index.ts | 25 +++++++++++++-- src/validate/html/validateElement.ts | 9 ++++-- src/validate/html/validateEventHandler.ts | 9 +++++- src/validate/html/validateWindow.ts | 4 +-- test/validator/index.js | 31 ++++++++++++------- .../event-handler-ref-invalid/errors.json | 8 +++++ .../event-handler-ref-invalid/input.html | 2 ++ .../samples/event-handler-ref/input.html | 2 ++ .../samples/event-handler-ref/warnings.json | 1 + 9 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 test/validator/samples/event-handler-ref-invalid/errors.json create mode 100644 test/validator/samples/event-handler-ref-invalid/input.html create mode 100644 test/validator/samples/event-handler-ref/input.html create mode 100644 test/validator/samples/event-handler-ref/warnings.json diff --git a/src/validate/html/index.ts b/src/validate/html/index.ts index b09cece0e402..ad653daa632c 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 69a50b6cff01..6658895c58f0 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 85fdad08e656..71d8a26fc5e3 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 7c78b7f37d11..e451bcc5344e 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 078299757303..9b4f4900b7b8 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 000000000000..3ec1eb61b67c --- /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 000000000000..f6cbe90c3a6b --- /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 000000000000..e4fa0dec0feb --- /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 000000000000..0637a088a01e --- /dev/null +++ b/test/validator/samples/event-handler-ref/warnings.json @@ -0,0 +1 @@ +[] \ No newline at end of file