From b914a2294a1aeb1d9d3ad1217e4cb9a03c586af4 Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Tue, 18 Apr 2017 10:12:03 -0400 Subject: [PATCH] validate bindings --- src/validate/html/index.js | 38 +++------ src/validate/html/validateElement.js | 85 +++++++++++++++++++ src/validate/html/validateWindow.js | 3 + src/validate/index.js | 5 +- src/validate/js/index.js | 4 +- .../binding-invalid-on-element/errors.json | 8 ++ .../binding-invalid-on-element/input.html | 1 + .../samples/binding-invalid/errors.json | 8 ++ .../samples/binding-invalid/input.html | 1 + 9 files changed, 121 insertions(+), 32 deletions(-) create mode 100644 src/validate/html/validateElement.js create mode 100644 src/validate/html/validateWindow.js create mode 100644 test/validator/samples/binding-invalid-on-element/errors.json create mode 100644 test/validator/samples/binding-invalid-on-element/input.html create mode 100644 test/validator/samples/binding-invalid/errors.json create mode 100644 test/validator/samples/binding-invalid/input.html diff --git a/src/validate/html/index.js b/src/validate/html/index.js index 8cad9bd849..7c791a600b 100644 --- a/src/validate/html/index.js +++ b/src/validate/html/index.js @@ -1,12 +1,12 @@ import * as namespaces from '../../utils/namespaces.js'; -import flattenReference from '../../utils/flattenReference.js'; +import validateElement from './validateElement.js'; +import validateWindow from './validateWindow.js'; const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|switch|symbol|text|textPath|title|tref|tspan|unknown|use|view|vkern)$/; -function list ( items, conjunction = 'or' ) { - if ( items.length === 1 ) return items[0]; - return `${items.slice( 0, -1 ).join( ', ' )} ${conjunction} ${items[ items.length - 1 ]}`; -} +const meta = { + ':Window': validateWindow +}; export default function validateHtml ( validator, html ) { let elementDepth = 0; @@ -17,31 +17,13 @@ export default function validateHtml ( validator, html ) { validator.warn( `<${node.name}> is an SVG element – did you forget to add { namespace: 'svg' } ?`, node.start ); } - elementDepth += 1; - - node.attributes.forEach( attribute => { - 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' && callee.name === 'set' || callee.name === 'fire' || callee.name in validator.methods ) return; - - const validCallees = list( [ 'this.*', 'event.*', 'set', 'fire' ].concat( Object.keys( validator.methods ) ) ); - let message = `'${validator.source.slice( callee.start, callee.end )}' is an invalid callee (should be one of ${validCallees})`; + if ( node.name in meta ) { + return meta[ node.name ]( validator, node ); + } - if ( callee.type === 'Identifier' && callee.name in validator.helpers ) { - message += `. '${callee.name}' exists on 'helpers', did you put it in the wrong place?`; - } + elementDepth += 1; - validator.error( message, start ); - } - }); + validateElement( validator, node ); } if ( node.children ) { diff --git a/src/validate/html/validateElement.js b/src/validate/html/validateElement.js new file mode 100644 index 0000000000..83a81c03c7 --- /dev/null +++ b/src/validate/html/validateElement.js @@ -0,0 +1,85 @@ +import flattenReference from '../../utils/flattenReference.js'; + +export default function validateElement ( validator, node ) { + const isComponent = node.name === ':Self' || validator.components.has( node.name ); + + node.attributes.forEach( attribute => { + if ( !isComponent && attribute.type === 'Binding' ) { + const { name } = attribute; + + if ( name === 'value' ) { + if ( node.name !== 'input' && node.name !== 'textarea' && node.name !== 'select' ) { + validator.error( `'value' is not a valid binding on <${node.name}> elements`, attribute.start ); + } + } + + else if ( name === 'checked' ) { + if ( node.name !== 'input' ) { + validator.error( `'checked' is not a valid binding on <${node.name}> elements`, attribute.start ); + } + + if ( getType( validator, node ) !== 'checkbox' ) { + validator.error( `'checked' binding can only be used with ` ); + } + } + + else if ( name === 'group' ) { + if ( node.name !== 'input' ) { + validator.error( `'group' is not a valid binding on <${node.name}> elements`, attribute.start ); + } + + const type = getType( validator, node ); + + if ( type !== 'checkbox' && type !== 'radio' ) { + validator.error( `'checked' binding can only be used with or ` ); + } + } + + else { + validator.error( `'${attribute.name}' is not a valid binding`, attribute.start ); + } + } + + 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' && callee.name === 'set' || callee.name === 'fire' || validator.methods.has( callee.name ) ) return; + + const validCallees = list( [ 'this.*', 'event.*', 'set', 'fire' ].concat( Array.from( validator.methods.keys() ) ) ); + let message = `'${validator.source.slice( callee.start, callee.end )}' is an invalid callee (should be one of ${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 ); + } + }); +} + +function getType ( validator, node ) { + const attribute = node.attributes.find( attribute => attribute.name === 'type' ); + if ( !attribute ) return null; + + if ( attribute.value === true ) { + validator.error( `'type' attribute must be specified`, attribute.start ); + } + + if ( attribute.value.length > 1 || attribute.value[0].type !== 'Text' ) { + validator.error( `'type attribute cannot be dynamic`, attribute.start ); + } + + 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 ]}`; +} \ No newline at end of file diff --git a/src/validate/html/validateWindow.js b/src/validate/html/validateWindow.js new file mode 100644 index 0000000000..233eefa922 --- /dev/null +++ b/src/validate/html/validateWindow.js @@ -0,0 +1,3 @@ +export default function validateWindow () { + // TODO +} \ No newline at end of file diff --git a/src/validate/index.js b/src/validate/index.js index d83161168e..8040549e56 100644 --- a/src/validate/index.js +++ b/src/validate/index.js @@ -41,8 +41,9 @@ export default function validate ( parsed, source, { onerror, onwarn, name, file namespace: null, defaultExport: null, properties: {}, - methods: {}, - helpers: {} + components: new Map(), + methods: new Map(), + helpers: new Map() }; try { diff --git a/src/validate/js/index.js b/src/validate/js/index.js index 3f7e8c0e42..013e75f04c 100644 --- a/src/validate/js/index.js +++ b/src/validate/js/index.js @@ -73,10 +73,10 @@ export default function validateJs ( validator, js ) { } }); - [ 'methods', 'helpers' ].forEach( key => { + [ 'components', 'methods', 'helpers' ].forEach( key => { if ( validator.properties[ key ] ) { validator.properties[ key ].value.properties.forEach( prop => { - validator[ key ][ prop.key.name ] = prop.value; + validator[ key ].set( prop.key.name, prop.value ); }); } }); diff --git a/test/validator/samples/binding-invalid-on-element/errors.json b/test/validator/samples/binding-invalid-on-element/errors.json new file mode 100644 index 0000000000..b83530c1ac --- /dev/null +++ b/test/validator/samples/binding-invalid-on-element/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "'value' is not a valid binding on
elements", + "pos": 5, + "loc": { + "line": 1, + "column": 5 + } +}] \ No newline at end of file diff --git a/test/validator/samples/binding-invalid-on-element/input.html b/test/validator/samples/binding-invalid-on-element/input.html new file mode 100644 index 0000000000..9b617e62be --- /dev/null +++ b/test/validator/samples/binding-invalid-on-element/input.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/test/validator/samples/binding-invalid/errors.json b/test/validator/samples/binding-invalid/errors.json new file mode 100644 index 0000000000..3de6e4b913 --- /dev/null +++ b/test/validator/samples/binding-invalid/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "'whatever' is not a valid binding", + "pos": 5, + "loc": { + "line": 1, + "column": 5 + } +}] \ No newline at end of file diff --git a/test/validator/samples/binding-invalid/input.html b/test/validator/samples/binding-invalid/input.html new file mode 100644 index 0000000000..8e5f874e5b --- /dev/null +++ b/test/validator/samples/binding-invalid/input.html @@ -0,0 +1 @@ +
\ No newline at end of file