From 2d7d7fec2a7a779144d4bfe6ad12323bf99248ea Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Thu, 4 May 2017 12:25:20 -0400 Subject: [PATCH] validate <:Window> --- package.json | 3 +- .../dom/visitors/Element/meta/Window.js | 5 -- src/validate/html/validateElement.js | 38 +------------- src/validate/html/validateEventHandler.js | 35 +++++++++++++ src/validate/html/validateWindow.js | 49 ++++++++++++++++++- src/validate/js/index.js | 10 ++-- src/validate/js/propValidators/namespace.js | 9 ++-- src/validate/{js => }/utils/FuzzySet.js | 0 src/validate/utils/fuzzymatch.js | 10 ++++ src/validate/utils/list.js | 4 ++ .../errors.json | 8 +++ .../input.html | 1 + .../window-binding-invalid-value/errors.json | 8 +++ .../window-binding-invalid-value/input.html | 1 + .../window-binding-invalid-width/errors.json | 8 +++ .../window-binding-invalid-width/input.html | 1 + .../window-binding-invalid/errors.json | 8 +++ .../samples/window-binding-invalid/input.html | 1 + .../samples/window-event-invalid/errors.json | 8 +++ .../samples/window-event-invalid/input.html | 1 + 20 files changed, 153 insertions(+), 55 deletions(-) create mode 100644 src/validate/html/validateEventHandler.js rename src/validate/{js => }/utils/FuzzySet.js (100%) create mode 100644 src/validate/utils/fuzzymatch.js create mode 100644 src/validate/utils/list.js create mode 100644 test/validator/samples/window-binding-invalid-innerwidth/errors.json create mode 100644 test/validator/samples/window-binding-invalid-innerwidth/input.html create mode 100644 test/validator/samples/window-binding-invalid-value/errors.json create mode 100644 test/validator/samples/window-binding-invalid-value/input.html create mode 100644 test/validator/samples/window-binding-invalid-width/errors.json create mode 100644 test/validator/samples/window-binding-invalid-width/input.html create mode 100644 test/validator/samples/window-binding-invalid/errors.json create mode 100644 test/validator/samples/window-binding-invalid/input.html create mode 100644 test/validator/samples/window-event-invalid/errors.json create mode 100644 test/validator/samples/window-event-invalid/input.html diff --git a/package.json b/package.json index 106036084c..ea6060fcdf 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,8 @@ }, "nyc": { "include": [ - "src/**/*.js" + "src/**/*.js", + "shared.js" ], "exclude": [ "src/**/__test__.js", diff --git a/src/generators/dom/visitors/Element/meta/Window.js b/src/generators/dom/visitors/Element/meta/Window.js index 4af5984811..2478d17139 100644 --- a/src/generators/dom/visitors/Element/meta/Window.js +++ b/src/generators/dom/visitors/Element/meta/Window.js @@ -60,11 +60,6 @@ export default function visitWindow ( generator, block, node ) { } if ( attribute.type === 'Binding' ) { - if ( attribute.value.type !== 'Identifier' ) { - const { parts, keypath } = flattenReference( attribute.value ); - throw new Error( `Bindings on <:Window/> must be to top-level properties, e.g. '${parts.pop()}' rather than '${keypath}'` ); - } - // in dev mode, throw if read-only values are written to if ( readonly.has( attribute.name ) ) { generator.readonly.add( attribute.value.name ); diff --git a/src/validate/html/validateElement.js b/src/validate/html/validateElement.js index cda56ec730..42e4c75f30 100644 --- a/src/validate/html/validateElement.js +++ b/src/validate/html/validateElement.js @@ -1,10 +1,4 @@ -import flattenReference from '../../utils/flattenReference.js'; - -const validBuiltins = new Set([ - 'set', - 'fire', - 'destroy' -]); +import validateEventHandler from './validateEventHandler.js'; export default function validateElement ( validator, node ) { const isComponent = node.name === ':Self' || validator.components.has( node.name ); @@ -53,30 +47,7 @@ export default function validateElement ( validator, node ) { } if ( attribute.type === 'EventHandler' ) { - const { callee, start, type } = attribute.expression; - - if ( type !== 'CallExpression' ) { - validator.error( `Expected a call expression`, start ); - } - - const { name } = flattenReference( callee ); - - if ( name === 'this' || name === 'event' ) return; - if ( callee.type === 'Identifier' && validBuiltins.has( callee.name ) || validator.methods.has( callee.name ) ) return; - - const validCallees = [ 'this.*', 'event.*' ] - .concat( - Array.from( validBuiltins ), - Array.from( validator.methods.keys() ) - ); - - let message = `'${validator.source.slice( callee.start, callee.end )}' is an invalid callee (should be one of ${list( validCallees )})`; - - if ( callee.type === 'Identifier' && validator.helpers.has( callee.name ) ) { - message += `. '${callee.name}' exists on 'helpers', did you put it in the wrong place?`; - } - - validator.error( message, start ); + validateEventHandler( validator, attribute ); } }); } @@ -95,8 +66,3 @@ function getType ( validator, node ) { return attribute.value[0].data; } - -function list ( items, conjunction = 'or' ) { - if ( items.length === 1 ) return items[0]; - return `${items.slice( 0, -1 ).join( ', ' )} ${conjunction} ${items[ items.length - 1 ]}`; -} diff --git a/src/validate/html/validateEventHandler.js b/src/validate/html/validateEventHandler.js new file mode 100644 index 0000000000..3c0f1f288d --- /dev/null +++ b/src/validate/html/validateEventHandler.js @@ -0,0 +1,35 @@ +import flattenReference from '../../utils/flattenReference.js'; +import list from '../utils/list.js'; + +const validBuiltins = new Set([ + 'set', + 'fire', + 'destroy' +]); + +export default function validateEventHandlerCallee ( validator, attribute ) { + const { callee, start, type } = attribute.expression; + + if ( type !== 'CallExpression' ) { + validator.error( `Expected a call expression`, start ); + } + + const { name } = flattenReference( callee ); + + if ( name === 'this' || name === 'event' ) return; + if ( callee.type === 'Identifier' && validBuiltins.has( callee.name ) || validator.methods.has( callee.name ) ) return; + + const validCallees = [ 'this.*', 'event.*' ] + .concat( + Array.from( validBuiltins ), + Array.from( validator.methods.keys() ) + ); + + let message = `'${validator.source.slice( callee.start, callee.end )}' is an invalid callee (should be one of ${list( validCallees )})`; + + if ( callee.type === 'Identifier' && validator.helpers.has( callee.name ) ) { + message += `. '${callee.name}' exists on 'helpers', did you put it in the wrong place?`; + } + + validator.error( message, start ); +} \ No newline at end of file diff --git a/src/validate/html/validateWindow.js b/src/validate/html/validateWindow.js index 233eefa922..e719691622 100644 --- a/src/validate/html/validateWindow.js +++ b/src/validate/html/validateWindow.js @@ -1,3 +1,48 @@ -export default function validateWindow () { - // TODO +import flattenReference from '../../utils/flattenReference.js'; +import fuzzymatch from '../utils/fuzzymatch.js'; +import list from '../utils/list.js'; +import validateEventHandler from './validateEventHandler.js'; + +const validBindings = [ + 'innerWidth', + 'innerHeight', + 'outerWidth', + 'outerHeight', + 'scrollX', + 'scrollY' +]; + +export default function validateWindow ( validator, node ) { + node.attributes.forEach( attribute => { + if ( attribute.type === 'Binding' ) { + if ( attribute.value.type !== 'Identifier' ) { + const { parts } = flattenReference( attribute.value ); + + validator.error( + `Bindings on <:Window/> must be to top-level properties, e.g. '${parts[ parts.length - 1 ]}' rather than '${parts.join( '.' )}'`, + attribute.value.start + ); + } + + if ( !~validBindings.indexOf( attribute.name ) ) { + const match = ( + attribute.name === 'width' ? 'innerWidth' : + attribute.name === 'height' ? 'innerHeight' : + fuzzymatch( attribute.name, validBindings ) + ); + + const message = `'${attribute.name}' is not a valid binding on <:Window>`; + + if ( match ) { + validator.error( `${message} (did you mean '${match}'?)`, attribute.start ); + } else { + validator.error( `${message} — valid bindings are ${list( validBindings )}`, attribute.start ); + } + } + } + + else if ( attribute.type === 'EventHandler' ) { + validateEventHandler( validator, attribute ); + } + }); } \ No newline at end of file diff --git a/src/validate/js/index.js b/src/validate/js/index.js index 6817382496..4d237e9f01 100644 --- a/src/validate/js/index.js +++ b/src/validate/js/index.js @@ -1,13 +1,11 @@ import propValidators from './propValidators/index.js'; -import FuzzySet from './utils/FuzzySet.js'; +import fuzzymatch from '../utils/fuzzymatch.js'; import checkForDupes from './utils/checkForDupes.js'; import checkForComputedKeys from './utils/checkForComputedKeys.js'; import namespaces from '../../utils/namespaces.js'; const validPropList = Object.keys( propValidators ); -const fuzzySet = new FuzzySet( validPropList ); - export default function validateJs ( validator, js ) { js.content.body.forEach( node => { // check there are no named exports @@ -45,9 +43,9 @@ export default function validateJs ( validator, js ) { if ( propValidator ) { propValidator( validator, prop ); } else { - const matches = fuzzySet.get( prop.key.name ); - if ( matches && matches[0] && matches[0][0] > 0.7 ) { - validator.error( `Unexpected property '${prop.key.name}' (did you mean '${matches[0][1]}'?)`, prop.start ); + const match = fuzzymatch( prop.key.name, validPropList ); + if ( match ) { + validator.error( `Unexpected property '${prop.key.name}' (did you mean '${match}'?)`, prop.start ); } else if ( /FunctionExpression/.test( prop.value.type ) ) { validator.error( `Unexpected property '${prop.key.name}' (did you mean to include it in 'methods'?)`, prop.start ); } else { diff --git a/src/validate/js/propValidators/namespace.js b/src/validate/js/propValidators/namespace.js index ec8bad0a04..be011c45d7 100644 --- a/src/validate/js/propValidators/namespace.js +++ b/src/validate/js/propValidators/namespace.js @@ -1,7 +1,6 @@ import * as namespaces from '../../../utils/namespaces.js'; -import FuzzySet from '../utils/FuzzySet.js'; +import fuzzymatch from '../../utils/fuzzymatch.js'; -const fuzzySet = new FuzzySet( namespaces.validNamespaces ); const valid = new Set( namespaces.validNamespaces ); export default function namespace ( validator, prop ) { @@ -12,9 +11,9 @@ export default function namespace ( validator, prop ) { } if ( !valid.has( ns ) ) { - const matches = fuzzySet.get( ns ); - if ( matches && matches[0] && matches[0][0] > 0.7 ) { - validator.error( `Invalid namespace '${ns}' (did you mean '${matches[0][1]}'?)`, prop.start ); + const match = fuzzymatch( ns, namespaces.validNamespaces ); + if ( match ) { + validator.error( `Invalid namespace '${ns}' (did you mean '${match}'?)`, prop.start ); } else { validator.error( `Invalid namespace '${ns}'`, prop.start ); } diff --git a/src/validate/js/utils/FuzzySet.js b/src/validate/utils/FuzzySet.js similarity index 100% rename from src/validate/js/utils/FuzzySet.js rename to src/validate/utils/FuzzySet.js diff --git a/src/validate/utils/fuzzymatch.js b/src/validate/utils/fuzzymatch.js new file mode 100644 index 0000000000..7eabd79090 --- /dev/null +++ b/src/validate/utils/fuzzymatch.js @@ -0,0 +1,10 @@ +import FuzzySet from './FuzzySet.js'; + +export default function fuzzymatch ( name, names ) { + const set = new FuzzySet( names ); + const matches = set.get( name ); + + return matches && matches[0] && matches[0][0] > 0.7 ? + matches[0][1] : + null; +} \ No newline at end of file diff --git a/src/validate/utils/list.js b/src/validate/utils/list.js new file mode 100644 index 0000000000..bada8167ec --- /dev/null +++ b/src/validate/utils/list.js @@ -0,0 +1,4 @@ +export default function list ( items, conjunction = 'or' ) { + if ( items.length === 1 ) return items[0]; + return `${items.slice( 0, -1 ).join( ', ' )} ${conjunction} ${items[ items.length - 1 ]}`; +} \ No newline at end of file diff --git a/test/validator/samples/window-binding-invalid-innerwidth/errors.json b/test/validator/samples/window-binding-invalid-innerwidth/errors.json new file mode 100644 index 0000000000..d4c5e99d18 --- /dev/null +++ b/test/validator/samples/window-binding-invalid-innerwidth/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "'innerwidth' is not a valid binding on <:Window> (did you mean 'innerWidth'?)", + "loc": { + "line": 1, + "column": 9 + }, + "pos": 9 +}] \ No newline at end of file diff --git a/test/validator/samples/window-binding-invalid-innerwidth/input.html b/test/validator/samples/window-binding-invalid-innerwidth/input.html new file mode 100644 index 0000000000..867e2c92dd --- /dev/null +++ b/test/validator/samples/window-binding-invalid-innerwidth/input.html @@ -0,0 +1 @@ +<:Window bind:innerwidth='w'/> \ No newline at end of file diff --git a/test/validator/samples/window-binding-invalid-value/errors.json b/test/validator/samples/window-binding-invalid-value/errors.json new file mode 100644 index 0000000000..7f0c3f8b25 --- /dev/null +++ b/test/validator/samples/window-binding-invalid-value/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "Bindings on <:Window/> must be to top-level properties, e.g. 'baz' rather than 'foo.bar.baz'", + "loc": { + "line": 1, + "column": 26 + }, + "pos": 26 +}] \ No newline at end of file diff --git a/test/validator/samples/window-binding-invalid-value/input.html b/test/validator/samples/window-binding-invalid-value/input.html new file mode 100644 index 0000000000..aa0bb48fae --- /dev/null +++ b/test/validator/samples/window-binding-invalid-value/input.html @@ -0,0 +1 @@ +<:Window bind:innerWidth='foo.bar.baz'/> \ No newline at end of file diff --git a/test/validator/samples/window-binding-invalid-width/errors.json b/test/validator/samples/window-binding-invalid-width/errors.json new file mode 100644 index 0000000000..b24b359611 --- /dev/null +++ b/test/validator/samples/window-binding-invalid-width/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "'width' is not a valid binding on <:Window> (did you mean 'innerWidth'?)", + "loc": { + "line": 1, + "column": 9 + }, + "pos": 9 +}] \ No newline at end of file diff --git a/test/validator/samples/window-binding-invalid-width/input.html b/test/validator/samples/window-binding-invalid-width/input.html new file mode 100644 index 0000000000..5b8348a818 --- /dev/null +++ b/test/validator/samples/window-binding-invalid-width/input.html @@ -0,0 +1 @@ +<:Window bind:width='w'/> \ No newline at end of file diff --git a/test/validator/samples/window-binding-invalid/errors.json b/test/validator/samples/window-binding-invalid/errors.json new file mode 100644 index 0000000000..f2a36540ec --- /dev/null +++ b/test/validator/samples/window-binding-invalid/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "'potato' is not a valid binding on <:Window> — valid bindings are innerWidth, innerHeight, outerWidth, outerHeight, scrollX or scrollY", + "loc": { + "line": 1, + "column": 9 + }, + "pos": 9 +}] \ No newline at end of file diff --git a/test/validator/samples/window-binding-invalid/input.html b/test/validator/samples/window-binding-invalid/input.html new file mode 100644 index 0000000000..3897b664b5 --- /dev/null +++ b/test/validator/samples/window-binding-invalid/input.html @@ -0,0 +1 @@ +<:Window bind:potato='foo'/> \ No newline at end of file diff --git a/test/validator/samples/window-event-invalid/errors.json b/test/validator/samples/window-event-invalid/errors.json new file mode 100644 index 0000000000..425c657cb1 --- /dev/null +++ b/test/validator/samples/window-event-invalid/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "'resize' is an invalid callee (should be one of this.*, event.*, set, fire or destroy)", + "loc": { + "line": 1, + "column": 20 + }, + "pos": 20 +}] \ No newline at end of file diff --git a/test/validator/samples/window-event-invalid/input.html b/test/validator/samples/window-event-invalid/input.html new file mode 100644 index 0000000000..c9c32eecac --- /dev/null +++ b/test/validator/samples/window-event-invalid/input.html @@ -0,0 +1 @@ +<:Window on:resize='resize()'/> \ No newline at end of file