diff --git a/compiler/index.js b/compiler/index.js index 20bf55d9bb..8ee6777e96 100644 --- a/compiler/index.js +++ b/compiler/index.js @@ -1,12 +1,25 @@ import parse from './parse/index.js'; +import validate from './validate/index.js'; import generate from './generate/index.js'; -export function compile ( template, options = {} ) { - const parsed = parse( template, options ); - // TODO validate template - const generated = generate( parsed, template, options ); +export function compile ( source, options = {} ) { + const parsed = parse( source, options ); - return generated; + const { errors, warnings } = validate( parsed, source, options ); + + if ( errors.length ) { + // TODO optionally show all errors? + throw errors[0]; + } + + if ( warnings.length ) { + console.warn( `Svelte: ${warnings.length} ${warnings.length === 1 ? 'error' : 'errors'} in ${options.filename || 'template'}:` ); + warnings.forEach( warning => { + console.warn( `(${warning.loc.line}:${warning.loc.column}) – ${warning.message}` ); + }); + } + + return generate( parsed, source, options ); } -export { parse }; +export { parse, validate }; diff --git a/compiler/parse/index.js b/compiler/parse/index.js index b988dd9ee2..823a496b1e 100644 --- a/compiler/parse/index.js +++ b/compiler/parse/index.js @@ -2,37 +2,13 @@ import { locate } from 'locate-character'; import fragment from './state/fragment.js'; import { whitespace } from './patterns.js'; import { trimStart, trimEnd } from './utils/trim.js'; -import spaces from '../utils/spaces.js'; +import getCodeFrame from '../utils/getCodeFrame.js'; import hash from './utils/hash.js'; -function tabsToSpaces ( str ) { - return str.replace( /^\t+/, match => match.split( '\t' ).join( ' ' ) ); -} - function ParseError ( message, template, index ) { const { line, column } = locate( template, index ); - const lines = template.split( '\n' ); - - const frameStart = Math.max( 0, line - 2 ); - const frameEnd = Math.min( line + 3, lines.length ); - - const digits = String( frameEnd + 1 ).length; - const frame = lines - .slice( frameStart, frameEnd ) - .map( ( str, i ) => { - const isErrorLine = frameStart + i === line; - - let lineNum = String( i + frameStart + 1 ); - while ( lineNum.length < digits ) lineNum = ` ${lineNum}`; - - if ( isErrorLine ) { - const indicator = spaces( digits + 2 + tabsToSpaces( str.slice( 0, column ) ).length ) + '^'; - return `${lineNum}: ${tabsToSpaces( str )}\n${indicator}`; - } - return `${lineNum}: ${tabsToSpaces( str )}`; - }) - .join( '\n' ); + const frame = getCodeFrame( template, line, column ); this.name = 'ParseError'; this.message = `${message} (${line + 1}:${column})\n${frame}`; diff --git a/compiler/utils/getCodeFrame.js b/compiler/utils/getCodeFrame.js new file mode 100644 index 0000000000..90fd01e98e --- /dev/null +++ b/compiler/utils/getCodeFrame.js @@ -0,0 +1,31 @@ +import spaces from './spaces.js'; + +function tabsToSpaces ( str ) { + return str.replace( /^\t+/, match => match.split( '\t' ).join( ' ' ) ); +} + +export default function getCodeFrame ( source, line, column ) { + const lines = source.split( '\n' ); + + const frameStart = Math.max( 0, line - 2 ); + const frameEnd = Math.min( line + 3, lines.length ); + + const digits = String( frameEnd + 1 ).length; + + return lines + .slice( frameStart, frameEnd ) + .map( ( str, i ) => { + const isErrorLine = frameStart + i === line; + + let lineNum = String( i + frameStart + 1 ); + while ( lineNum.length < digits ) lineNum = ` ${lineNum}`; + + if ( isErrorLine ) { + const indicator = spaces( digits + 2 + tabsToSpaces( str.slice( 0, column ) ).length ) + '^'; + return `${lineNum}: ${tabsToSpaces( str )}\n${indicator}`; + } + + return `${lineNum}: ${tabsToSpaces( str )}`; + }) + .join( '\n' ); +} diff --git a/compiler/validate/index.js b/compiler/validate/index.js new file mode 100644 index 0000000000..3e47bb4790 --- /dev/null +++ b/compiler/validate/index.js @@ -0,0 +1,42 @@ +import validateJs from './js/index.js'; +import { getLocator } from 'locate-character'; + +export default function validate ( parsed, source ) { + const locator = getLocator( source ); + + const validator = { + error: ( message, pos ) => { + const { line, column } = locator( pos ); + + validator.errors.push({ + message, + pos, + loc: { line: line + 1, column } + }); + }, + + warn: ( message, pos ) => { + const { line, column } = locator( pos ); + + validator.warnings.push({ + message, + pos, + loc: { line: line + 1, column } + }); + }, + + templateProperties: {}, + + errors: [], + warnings: [] + }; + + if ( parsed.js ) { + validateJs( validator, parsed.js, source ); + } + + return { + errors: validator.errors, + warnings: validator.warnings + }; +} diff --git a/compiler/validate/js/index.js b/compiler/validate/js/index.js new file mode 100644 index 0000000000..bbe518b285 --- /dev/null +++ b/compiler/validate/js/index.js @@ -0,0 +1,52 @@ +import propValidators from './propValidators/index.js'; +import FuzzySet from './utils/FuzzySet.js'; +import checkForDupes from './utils/checkForDupes.js'; +import checkForComputedKeys from './utils/checkForComputedKeys.js'; + +const validPropList = Object.keys( propValidators ); + +const fuzzySet = new FuzzySet( validPropList ); + +export default function validateJs ( validator, js, source ) { + js.content.body.forEach( node => { + // check there are no named exports + if ( node.type === 'ExportNamedDeclaration' ) { + validator.error( `A component can only have a default export`, node.start ); + } + + if ( node.type === 'ExportDefaultDeclaration' ) { + if ( validator.defaultExport ) { + validator.error( `Duplicate default export`, node.start ); + } + + validator.defaultExport = node; + } + }); + + // ensure all exported props are valid + if ( validator.defaultExport ) { + checkForComputedKeys( validator, validator.defaultExport.declaration.properties ); + checkForDupes( validator, validator.defaultExport.declaration.properties ); + + validator.defaultExport.declaration.properties.forEach( prop => { + validator.templateProperties[ prop.key.value ] = prop; + }); + + validator.defaultExport.declaration.properties.forEach( prop => { + const propValidator = propValidators[ prop.key.name ]; + + 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 ); + } 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 { + validator.error( `Unexpected property '${prop.key.name}'`, prop.start ); + } + } + }); + } +} diff --git a/compiler/validate/js/propValidators/components.js b/compiler/validate/js/propValidators/components.js new file mode 100644 index 0000000000..b90d9babc2 --- /dev/null +++ b/compiler/validate/js/propValidators/components.js @@ -0,0 +1,3 @@ +export default function components ( validator, prop ) { + +} diff --git a/compiler/validate/js/propValidators/computed.js b/compiler/validate/js/propValidators/computed.js new file mode 100644 index 0000000000..102ce73333 --- /dev/null +++ b/compiler/validate/js/propValidators/computed.js @@ -0,0 +1,32 @@ +import checkForDupes from '../utils/checkForDupes.js'; +import checkForComputedKeys from '../utils/checkForComputedKeys.js'; + +const isFunctionExpression = { + FunctionExpression: true, + ArrowFunctionExpression: true +}; + +export default function computed ( validator, prop ) { + if ( prop.value.type !== 'ObjectExpression' ) { + validator.error( `The 'computed' property must be an object literal`, prop.start ); + return; + } + + checkForDupes( validator, prop.value.properties ); + checkForComputedKeys( validator, prop.value.properties ); + + prop.value.properties.forEach( computation => { + if ( !isFunctionExpression[ computation.value.type ] ) { + validator.error( `Computed properties can be function expressions or arrow function expressions`, computation.value.start ); + return; + } + + computation.value.params.forEach( param => { + const valid = param.type === 'Identifier' || param.type === 'AssignmentPattern' && param.left.type === 'Identifier'; + + if ( !valid ) { + validator.error( `Computed properties cannot use destructuring in function parameters`, param.start ); + } + }); + }); +} diff --git a/compiler/validate/js/propValidators/data.js b/compiler/validate/js/propValidators/data.js new file mode 100644 index 0000000000..89fe0763b6 --- /dev/null +++ b/compiler/validate/js/propValidators/data.js @@ -0,0 +1,3 @@ +export default function data ( validator, prop ) { + +} diff --git a/compiler/validate/js/propValidators/events.js b/compiler/validate/js/propValidators/events.js new file mode 100644 index 0000000000..ca7b280348 --- /dev/null +++ b/compiler/validate/js/propValidators/events.js @@ -0,0 +1,3 @@ +export default function events ( validator, prop ) { + +} diff --git a/compiler/validate/js/propValidators/helpers.js b/compiler/validate/js/propValidators/helpers.js new file mode 100644 index 0000000000..71916a5623 --- /dev/null +++ b/compiler/validate/js/propValidators/helpers.js @@ -0,0 +1,3 @@ +export default function helpers ( validator, prop ) { + +} diff --git a/compiler/validate/js/propValidators/index.js b/compiler/validate/js/propValidators/index.js new file mode 100644 index 0000000000..dcbf004fff --- /dev/null +++ b/compiler/validate/js/propValidators/index.js @@ -0,0 +1,19 @@ +import data from './data.js'; +import computed from './computed.js'; +import onrender from './onrender.js'; +import onteardown from './onteardown.js'; +import helpers from './helpers.js'; +import methods from './methods.js'; +import components from './components.js'; +import events from './events.js'; + +export default { + data, + computed, + onrender, + onteardown, + helpers, + methods, + components, + events +}; diff --git a/compiler/validate/js/propValidators/methods.js b/compiler/validate/js/propValidators/methods.js new file mode 100644 index 0000000000..275c47db11 --- /dev/null +++ b/compiler/validate/js/propValidators/methods.js @@ -0,0 +1,3 @@ +export default function methods ( validator, prop ) { + +} diff --git a/compiler/validate/js/propValidators/onrender.js b/compiler/validate/js/propValidators/onrender.js new file mode 100644 index 0000000000..2eafe7abba --- /dev/null +++ b/compiler/validate/js/propValidators/onrender.js @@ -0,0 +1,3 @@ +export default function onrender ( validator, prop ) { + +} diff --git a/compiler/validate/js/propValidators/onteardown.js b/compiler/validate/js/propValidators/onteardown.js new file mode 100644 index 0000000000..bbdade9e2b --- /dev/null +++ b/compiler/validate/js/propValidators/onteardown.js @@ -0,0 +1,3 @@ +export default function onteardown ( validator, prop ) { + +} diff --git a/compiler/validate/js/utils/FuzzySet.js b/compiler/validate/js/utils/FuzzySet.js new file mode 100644 index 0000000000..f4b5d6502c --- /dev/null +++ b/compiler/validate/js/utils/FuzzySet.js @@ -0,0 +1,290 @@ +// adapted from https://github.com/Glench/this.js/blob/master/lib/this.js +// BSD Licensed + +export default function FuzzySet (arr, useLevenshtein, gramSizeLower, gramSizeUpper) { + // default options + arr = arr || []; + this.gramSizeLower = gramSizeLower || 2; + this.gramSizeUpper = gramSizeUpper || 3; + this.useLevenshtein = (typeof useLevenshtein !== 'boolean') ? true : useLevenshtein; + + // define all the object functions and attributes + this.exactSet = {}; + this.matchDict = {}; + this.items = {}; + + // helper functions + function levenshtein ( str1, str2 ) { + const current = []; + let prev; + let value; + + for (let i = 0; i <= str2.length; i++) { + for (let j = 0; j <= str1.length; j++) { + if (i && j) { + if (str1.charAt(j - 1) === str2.charAt(i - 1)) { + value = prev; + } else { + value = Math.min(current[j], current[j - 1], prev) + 1; + } + } else { + value = i + j; + } + + prev = current[j]; + current[j] = value; + } + } + + return current.pop(); + } + + // return an edit distance from 0 to 1 + function _distance (str1, str2) { + if (str1 === null && str2 === null) throw 'Trying to compare two null values'; + if (str1 === null || str2 === null) return 0; + str1 = String(str1); str2 = String(str2); + + const distance = levenshtein(str1, str2); + if (str1.length > str2.length) { + return 1 - distance / str1.length; + } else { + return 1 - distance / str2.length; + } + } + + const _nonWordRe = /[^\w, ]+/; + + function _iterateGrams (value, gramSize) { + gramSize = gramSize || 2; + const simplified = '-' + value.toLowerCase().replace(_nonWordRe, '') + '-'; + const lenDiff = gramSize - simplified.length; + const results = []; + + if (lenDiff > 0) { + for (let i = 0; i < lenDiff; ++i) { + value += '-'; + } + } + for (let i = 0; i < simplified.length - gramSize + 1; ++i) { + results.push(simplified.slice(i, i + gramSize)); + } + return results; + } + + function _gramCounter (value, gramSize) { + // return an object where key=gram, value=number of occurrences + gramSize = gramSize || 2; + const result = {}; + const grams = _iterateGrams(value, gramSize); + let i = 0; + + for (i; i < grams.length; ++i) { + if (grams[i] in result) { + result[grams[i]] += 1; + } else { + result[grams[i]] = 1; + } + } + return result; + } + + // the main functions + this.get = function (value, defaultValue) { + // check for value in set, returning defaultValue or null if none found + const result = this._get(value); + if (!result && typeof defaultValue !== 'undefined') { + return defaultValue; + } + return result; + }; + + this._get = function (value) { + const normalizedValue = this._normalizeStr(value); + const result = this.exactSet[normalizedValue]; + + if (result) { + return [[1, result]]; + } + + let results = []; + // start with high gram size and if there are no results, go to lower gram sizes + for (let gramSize = this.gramSizeUpper; gramSize >= this.gramSizeLower; --gramSize) { + results = this.__get(value, gramSize); + if (results) { + return results; + } + } + return null; + }; + + this.__get = function (value, gramSize) { + const normalizedValue = this._normalizeStr(value); + const matches = {}; + const gramCounts = _gramCounter(normalizedValue, gramSize); + const items = this.items[gramSize]; + let sumOfSquareGramCounts = 0; + let gram; + let gramCount; + let i; + let index; + let otherGramCount; + + for (gram in gramCounts) { + gramCount = gramCounts[gram]; + sumOfSquareGramCounts += Math.pow(gramCount, 2); + if (gram in this.matchDict) { + for (i = 0; i < this.matchDict[gram].length; ++i) { + index = this.matchDict[gram][i][0]; + otherGramCount = this.matchDict[gram][i][1]; + if (index in matches) { + matches[index] += gramCount * otherGramCount; + } else { + matches[index] = gramCount * otherGramCount; + } + } + } + } + + function isEmptyObject ( obj ) { + for ( const prop in obj ) { + if ( obj.hasOwnProperty( prop ) ) + return false; + } + return true; + } + + if (isEmptyObject(matches)) { + return null; + } + + const vectorNormal = Math.sqrt(sumOfSquareGramCounts); + let results = []; + let matchScore; + + // build a results list of [score, str] + for (const matchIndex in matches) { + matchScore = matches[matchIndex]; + results.push([matchScore / (vectorNormal * items[matchIndex][0]), items[matchIndex][1]]); + } + function sortDescending (a, b) { + if (a[0] < b[0]) { + return 1; + } else if (a[0] > b[0]) { + return -1; + } else { + return 0; + } + } + + results.sort(sortDescending); + if (this.useLevenshtein) { + const newResults = []; + const endIndex = Math.min(50, results.length); + // truncate somewhat arbitrarily to 50 + for (let i = 0; i < endIndex; ++i) { + newResults.push([_distance(results[i][1], normalizedValue), results[i][1]]); + } + results = newResults; + results.sort(sortDescending); + } + const newResults = []; + for (let i = 0; i < results.length; ++i) { + if (results[i][0] == results[0][0]) { + newResults.push([results[i][0], this.exactSet[results[i][1]]]); + } + } + return newResults; + }; + + this.add = function (value) { + const normalizedValue = this._normalizeStr(value); + if (normalizedValue in this.exactSet) { + return false; + } + + let i = this.gramSizeLower; + for (i; i < this.gramSizeUpper + 1; ++i) { + this._add(value, i); + } + }; + + this._add = function (value, gramSize) { + const normalizedValue = this._normalizeStr(value); + const items = this.items[gramSize] || []; + const index = items.length; + + items.push(0); + const gramCounts = _gramCounter(normalizedValue, gramSize); + let sumOfSquareGramCounts = 0; + let gram; + let gramCount; + + for (gram in gramCounts) { + gramCount = gramCounts[gram]; + sumOfSquareGramCounts += Math.pow(gramCount, 2); + if (gram in this.matchDict) { + this.matchDict[gram].push([index, gramCount]); + } else { + this.matchDict[gram] = [[index, gramCount]]; + } + } + const vectorNormal = Math.sqrt(sumOfSquareGramCounts); + items[index] = [vectorNormal, normalizedValue]; + this.items[gramSize] = items; + this.exactSet[normalizedValue] = value; + }; + + this._normalizeStr = function (str) { + if (Object.prototype.toString.call(str) !== '[object String]') throw 'Must use a string as argument to FuzzySet functions'; + return str.toLowerCase(); + }; + + // return length of items in set + this.length = function () { + let count = 0; + let prop; + + for (prop in this.exactSet) { + if (this.exactSet.hasOwnProperty(prop)) { + count += 1; + } + } + return count; + }; + + // return is set is empty + this.isEmpty = function () { + for (const prop in this.exactSet) { + if (this.exactSet.hasOwnProperty(prop)) { + return false; + } + } + return true; + }; + + // return list of values loaded into set + this.values = function () { + const values = []; + + for (const prop in this.exactSet) { + if (this.exactSet.hasOwnProperty(prop)) { + values.push(this.exactSet[prop]); + } + } + return values; + }; + + + // initialization + let i = this.gramSizeLower; + for (i; i < this.gramSizeUpper + 1; ++i) { + this.items[i] = []; + } + // add all the items to the set + for (i = 0; i < arr.length; ++i) { + this.add(arr[i]); + } + + return this; +} diff --git a/compiler/validate/js/utils/checkForComputedKeys.js b/compiler/validate/js/utils/checkForComputedKeys.js new file mode 100644 index 0000000000..f4365fde9d --- /dev/null +++ b/compiler/validate/js/utils/checkForComputedKeys.js @@ -0,0 +1,7 @@ +export default function checkForComputedKeys ( validator, properties ) { + properties.forEach( prop => { + if ( prop.key.computed ) { + validator.error( `Cannot use computed keys`, prop.start ); + } + }); +} diff --git a/compiler/validate/js/utils/checkForDupes.js b/compiler/validate/js/utils/checkForDupes.js new file mode 100644 index 0000000000..ef7a640851 --- /dev/null +++ b/compiler/validate/js/utils/checkForDupes.js @@ -0,0 +1,11 @@ +export default function checkForDupes ( validator, properties ) { + const seen = Object.create( null ); + + properties.forEach( prop => { + if ( seen[ prop.key.name ] ) { + validator.error( `Duplicate property '${prop.key.name}'`, prop.start ); + } + + seen[ prop.key.name ] = true; + }); +} diff --git a/package.json b/package.json index ec7cecb50d..a15d87aa14 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "eslint": "^3.10.2", "eslint-plugin-import": "^2.2.0", "estree-walker": "^0.3.0", + "fuzzyset.js": "0.0.1", "jsdom": "^9.8.3", "locate-character": "^2.0.0", "magic-string": "^0.16.0", @@ -51,7 +52,9 @@ "reify": "^0.4.0", "rollup": "^0.36.3", "rollup-plugin-buble": "^0.14.0", - "rollup-plugin-node-resolve": "^2.0.0" + "rollup-plugin-commonjs": "^5.0.5", + "rollup-plugin-node-resolve": "^2.0.0", + "source-map-support": "^0.4.6" }, "nyc": { "include": [ diff --git a/rollup.config.js b/rollup.config.js index 93b495980e..0250fbf27e 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,4 +1,5 @@ import nodeResolve from 'rollup-plugin-node-resolve'; +import commonjs from 'rollup-plugin-commonjs'; export default { entry: 'compiler/index.js', @@ -7,6 +8,8 @@ export default { { dest: 'dist/svelte.js', format: 'umd' } ], plugins: [ - nodeResolve({ jsnext: true, module: true }) - ] + nodeResolve({ jsnext: true, module: true }), + commonjs() + ], + sourceMap: true }; diff --git a/test/test.js b/test/test.js index 27101e41aa..5cc883f017 100644 --- a/test/test.js +++ b/test/test.js @@ -1,11 +1,14 @@ -import { compile, parse } from '../dist/svelte.js'; +import { compile, parse, validate } from '../dist/svelte.js'; import assert from 'assert'; import * as path from 'path'; import * as fs from 'fs'; import jsdom from 'jsdom'; -import { install } from 'console-group'; -install(); +import * as consoleGroup from 'console-group'; +consoleGroup.install(); + +import * as sourceMapSupport from 'source-map-support'; +sourceMapSupport.install(); const cache = {}; @@ -24,7 +27,7 @@ function exists ( path ) { } describe( 'svelte', () => { - describe( 'parser', () => { + describe( 'parse', () => { fs.readdirSync( 'test/parser' ).forEach( dir => { if ( dir[0] === '.' ) return; @@ -57,7 +60,51 @@ describe( 'svelte', () => { }); }); - describe( 'compiler', () => { + describe( 'validate', () => { + function tryToLoadJson ( file ) { + try { + return JSON.parse( fs.readFileSync( file ) ); + } catch ( err ) { + if ( err.code !== 'ENOENT' ) throw err; + return null; + } + } + + fs.readdirSync( 'test/validator' ).forEach( dir => { + if ( dir[0] === '.' ) return; + + const solo = exists( `test/validator/${dir}/solo` ); + + ( solo ? it.only : it )( dir, () => { + const input = fs.readFileSync( `test/validator/${dir}/input.html`, 'utf-8' ).replace( /\s+$/, '' ); + + try { + const parsed = parse( input ); + const { errors, warnings } = validate( parsed, input ); + + const expectedErrors = tryToLoadJson( `test/validator/${dir}/errors.json` ) || []; + const expectedWarnings = tryToLoadJson( `test/validator/${dir}/warnings.json` ) || []; + + assert.deepEqual( errors, expectedErrors ); + assert.deepEqual( warnings, expectedWarnings ); + } catch ( err ) { + if ( err.name !== 'ParseError' ) throw err; + + try { + const expected = require( `./validator/${dir}/error.json` ); + + assert.equal( err.shortMessage, expected.message ); + assert.deepEqual( err.loc, expected.loc ); + assert.equal( err.pos, expected.pos ); + } catch ( err2 ) { + throw err2.code === 'MODULE_NOT_FOUND' ? err : err2; + } + } + }); + }); + }); + + describe( 'generate', () => { before( () => { function cleanChildren ( node ) { let previous = null; diff --git a/test/validator/named-export/errors.json b/test/validator/named-export/errors.json new file mode 100644 index 0000000000..ce27a91863 --- /dev/null +++ b/test/validator/named-export/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "A component can only have a default export", + "pos": 10, + "loc": { + "line": 2, + "column": 1 + } +}] diff --git a/test/validator/named-export/input.html b/test/validator/named-export/input.html new file mode 100644 index 0000000000..269a7cc214 --- /dev/null +++ b/test/validator/named-export/input.html @@ -0,0 +1,3 @@ + diff --git a/test/validator/properties-computed-must-be-an-object/errors.json b/test/validator/properties-computed-must-be-an-object/errors.json new file mode 100644 index 0000000000..c8c0392eea --- /dev/null +++ b/test/validator/properties-computed-must-be-an-object/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "The 'computed' property must be an object literal", + "loc": { + "line": 5, + "column": 2 + }, + "pos": 42 +}] diff --git a/test/validator/properties-computed-must-be-an-object/input.html b/test/validator/properties-computed-must-be-an-object/input.html new file mode 100644 index 0000000000..337fc3c386 --- /dev/null +++ b/test/validator/properties-computed-must-be-an-object/input.html @@ -0,0 +1,7 @@ +
+ + diff --git a/test/validator/properties-computed-must-be-functions/errors.json b/test/validator/properties-computed-must-be-functions/errors.json new file mode 100644 index 0000000000..958b07b3f6 --- /dev/null +++ b/test/validator/properties-computed-must-be-functions/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "Computed properties can be function expressions or arrow function expressions", + "loc": { + "line": 6, + "column": 8 + }, + "pos": 62 +}] diff --git a/test/validator/properties-computed-must-be-functions/input.html b/test/validator/properties-computed-must-be-functions/input.html new file mode 100644 index 0000000000..bea71c86cd --- /dev/null +++ b/test/validator/properties-computed-must-be-functions/input.html @@ -0,0 +1,9 @@ + + + diff --git a/test/validator/properties-computed-no-destructuring/errors.json b/test/validator/properties-computed-no-destructuring/errors.json new file mode 100644 index 0000000000..cc209ea249 --- /dev/null +++ b/test/validator/properties-computed-no-destructuring/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "Computed properties cannot use destructuring in function parameters", + "loc": { + "line": 6, + "column": 8 + }, + "pos": 62 +}] diff --git a/test/validator/properties-computed-no-destructuring/input.html b/test/validator/properties-computed-no-destructuring/input.html new file mode 100644 index 0000000000..ae88d47b9e --- /dev/null +++ b/test/validator/properties-computed-no-destructuring/input.html @@ -0,0 +1,9 @@ + + + diff --git a/test/validator/properties-duplicated/errors.json b/test/validator/properties-duplicated/errors.json new file mode 100644 index 0000000000..a743641a6c --- /dev/null +++ b/test/validator/properties-duplicated/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "Duplicate property 'methods'", + "loc": { + "line": 9, + "column": 2 + }, + "pos": 74 +}] diff --git a/test/validator/properties-duplicated/input.html b/test/validator/properties-duplicated/input.html new file mode 100644 index 0000000000..4945e8e320 --- /dev/null +++ b/test/validator/properties-duplicated/input.html @@ -0,0 +1,13 @@ + + + diff --git a/test/validator/properties-unexpected-b/errors.json b/test/validator/properties-unexpected-b/errors.json new file mode 100644 index 0000000000..bc1a41a69a --- /dev/null +++ b/test/validator/properties-unexpected-b/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "Unexpected property 'doSomething' (did you mean to include it in 'methods'?)", + "loc": { + "line": 5, + "column": 2 + }, + "pos": 42 +}] diff --git a/test/validator/properties-unexpected-b/input.html b/test/validator/properties-unexpected-b/input.html new file mode 100644 index 0000000000..e666f4e2b1 --- /dev/null +++ b/test/validator/properties-unexpected-b/input.html @@ -0,0 +1,9 @@ + + + diff --git a/test/validator/properties-unexpected/errors.json b/test/validator/properties-unexpected/errors.json new file mode 100644 index 0000000000..e4d4c58f78 --- /dev/null +++ b/test/validator/properties-unexpected/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "Unexpected property 'dada' (did you mean 'data'?)", + "loc": { + "line": 5, + "column": 2 + }, + "pos": 42 +}] diff --git a/test/validator/properties-unexpected/input.html b/test/validator/properties-unexpected/input.html new file mode 100644 index 0000000000..626e916993 --- /dev/null +++ b/test/validator/properties-unexpected/input.html @@ -0,0 +1,11 @@ + + +