diff --git a/src/parse/interfaces.ts b/src/interfaces.ts similarity index 100% rename from src/parse/interfaces.ts rename to src/interfaces.ts diff --git a/src/parse/index.ts b/src/parse/index.ts index b169312acc..5c9e2257fe 100644 --- a/src/parse/index.ts +++ b/src/parse/index.ts @@ -4,29 +4,13 @@ import { whitespace } from '../utils/patterns'; import { trimStart, trimEnd } from '../utils/trim'; import getCodeFrame from '../utils/getCodeFrame'; import hash from './utils/hash'; -import { Node } from './interfaces'; - -class ParseError extends Error { - frame: string - loc: { line: number, column: number } - pos: number - filename: string +import { Node } from '../interfaces'; +import CompileError from '../utils/CompileError' +class ParseError extends CompileError { constructor ( message: string, template: string, index: number, filename: string ) { - super( message ); - - const { line, column } = locate( template, index ); - + super( message, template, index, filename ); this.name = 'ParseError'; - this.loc = { line: line + 1, column }; - this.pos = index; - this.filename = filename; - - this.frame = getCodeFrame( template, line, column ); - } - - toString () { - return `${this.message} (${this.loc.line}:${this.loc.column})\n${this.frame}`; } } diff --git a/src/parse/state/mustache.ts b/src/parse/state/mustache.ts index a8efeb609f..afa439a45c 100644 --- a/src/parse/state/mustache.ts +++ b/src/parse/state/mustache.ts @@ -2,7 +2,7 @@ import readExpression from '../read/expression'; import { whitespace } from '../../utils/patterns'; import { trimStart, trimEnd } from '../../utils/trim'; import { Parser } from '../index'; -import { Node } from '../interfaces'; +import { Node } from '../../interfaces'; const validIdentifier = /[a-zA-Z_$][a-zA-Z0-9_$]*/; diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts index d8c64d3be5..97d34db372 100644 --- a/src/parse/state/tag.ts +++ b/src/parse/state/tag.ts @@ -6,7 +6,7 @@ import { trimStart, trimEnd } from '../../utils/trim'; import { decodeCharacterReferences } from '../utils/html'; import isVoidElementName from '../../utils/isVoidElementName'; import { Parser } from '../index'; -import { Node } from '../interfaces'; +import { Node } from '../../interfaces'; const validTagName = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/; const invalidUnquotedAttributeCharacters = /[\s"'=<>\/`]/; diff --git a/src/utils/CompileError.ts b/src/utils/CompileError.ts new file mode 100644 index 0000000000..85d713dc79 --- /dev/null +++ b/src/utils/CompileError.ts @@ -0,0 +1,25 @@ +import { locate } from 'locate-character'; +import getCodeFrame from '../utils/getCodeFrame'; + +export default class CompileError extends Error { + frame: string + loc: { line: number, column: number } + pos: number + filename: string + + constructor ( message: string, template: string, index: number, filename: string ) { + super( message ); + + const { line, column } = locate( template, index ); + + this.loc = { line: line + 1, column }; + this.pos = index; + this.filename = filename; + + this.frame = getCodeFrame( template, line, column ); + } + + toString () { + return `${this.message} (${this.loc.line}:${this.loc.column})\n${this.frame}`; + } +} \ No newline at end of file diff --git a/src/utils/isReference.ts b/src/utils/isReference.ts index 05d216f9ad..d486cf5c26 100644 --- a/src/utils/isReference.ts +++ b/src/utils/isReference.ts @@ -1,4 +1,6 @@ -export default function isReference ( node, parent ): boolean { +import { Node } from '../interfaces'; + +export default function isReference ( node: Node, parent: Node ): boolean { if ( node.type === 'MemberExpression' ) { return !node.computed && isReference( node.object, node ); } diff --git a/src/validate/index.ts b/src/validate/index.ts index d9240b12eb..64c4cdb095 100644 --- a/src/validate/index.ts +++ b/src/validate/index.ts @@ -1,52 +1,68 @@ import validateJs from './js/index'; import validateHtml from './html/index'; -import { getLocator } from 'locate-character'; +import { getLocator, Location } from 'locate-character'; import getCodeFrame from '../utils/getCodeFrame'; +import CompileError from '../utils/CompileError' +import { Node } from '../interfaces'; -export default function validate ( parsed, source, { onerror, onwarn, name, filename } ) { - const locator = getLocator( source ); +class ValidationError extends CompileError { + constructor ( message: string, template: string, index: number, filename: string ) { + super( message, template, index, filename ); + this.name = 'ValidationError'; + } +} - const validator = { - error: ( message, pos ) => { - const { line, column } = locator( pos ); +export class Validator { + readonly source: string; + readonly filename: string; - const error = new Error( message ); - error.frame = getCodeFrame( source, line, column ); - error.loc = { line: line + 1, column }; - error.pos = pos; - error.filename = filename; + onwarn: ({}) => void; + locator?: (pos: number) => Location; - error.toString = () => `${error.message} (${error.loc.line}:${error.loc.column})\n${error.frame}`; + namespace: string; + defaultExport: Node; + components: Map; + methods: Map; + helpers: Map; + transitions: Map; - throw error; - }, + constructor ( parsed, source: string, options: { onwarn, name?: string, filename?: string } ) { + this.source = source; + this.filename = options !== undefined ? options.filename : undefined; - warn: ( message, pos ) => { - const { line, column } = locator( pos ); + this.onwarn = options !== undefined ? options.onwarn : undefined; - const frame = getCodeFrame( source, line, column ); + this.namespace = null; + this.defaultExport = null; + this.properties = {}; + this.components = new Map(); + this.methods = new Map(); + this.helpers = new Map(); + this.transitions = new Map(); + } - onwarn({ - message, - frame, - loc: { line: line + 1, column }, - pos, - filename, - toString: () => `${message} (${line + 1}:${column})\n${frame}` - }); - }, + error ( message: string, pos: number ) { + throw new ValidationError( message, this.source, pos, this.filename ); + } - source, + warn ( message: string, pos: number ) { + if ( !this.locator ) this.locator = getLocator( this.source ); + const { line, column } = this.locator( pos ); - namespace: null, - defaultExport: null, - properties: {}, - components: new Map(), - methods: new Map(), - helpers: new Map(), - transitions: new Map() - }; + const frame = getCodeFrame( this.source, line, column ); + this.onwarn({ + message, + frame, + loc: { line: line + 1, column }, + pos, + filename: this.filename, + toString: () => `${message} (${line + 1}:${column})\n${frame}` + }); + } +} + +export default function validate ( parsed, source, { onerror, onwarn, name, filename } ) { try { if ( name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test( name ) ) { const error = new Error( `options.name must be a valid identifier` ); @@ -62,6 +78,12 @@ export default function validate ( parsed, source, { onerror, onwarn, name, file }); } + const validator = new Validator( parsed, source, { + onwarn, + name, + filename + }); + if ( parsed.js ) { validateJs( validator, parsed.js ); } diff --git a/src/validate/js/index.ts b/src/validate/js/index.ts index 9ccba4b944..44952ef5b1 100644 --- a/src/validate/js/index.ts +++ b/src/validate/js/index.ts @@ -3,11 +3,13 @@ import fuzzymatch from '../utils/fuzzymatch'; import checkForDupes from './utils/checkForDupes'; import checkForComputedKeys from './utils/checkForComputedKeys'; import namespaces from '../../utils/namespaces'; +import { Validator } from '../'; +import { Node } from '../../interfaces'; const validPropList = Object.keys( propValidators ); -export default function validateJs ( validator, js ) { - js.content.body.forEach( node => { +export default function validateJs ( validator: Validator, js ) { + js.content.body.forEach( ( node: Node ) => { // check there are no named exports if ( node.type === 'ExportNamedDeclaration' ) { validator.error( `A component can only have a default export`, node.start ); @@ -23,7 +25,7 @@ export default function validateJs ( validator, js ) { const props = validator.properties; - node.declaration.properties.forEach( prop => { + node.declaration.properties.forEach( ( prop: Node ) => { props[ prop.key.name ] = prop; }); @@ -37,7 +39,7 @@ export default function validateJs ( validator, js ) { } // ensure all exported props are valid - node.declaration.properties.forEach( prop => { + node.declaration.properties.forEach( ( prop: Node ) => { const propValidator = propValidators[ prop.key.name ]; if ( propValidator ) { diff --git a/src/validate/js/propValidators/components.ts b/src/validate/js/propValidators/components.ts index 78e9a1c57c..36077c52ed 100644 --- a/src/validate/js/propValidators/components.ts +++ b/src/validate/js/propValidators/components.ts @@ -1,7 +1,9 @@ import checkForDupes from '../utils/checkForDupes'; import checkForComputedKeys from '../utils/checkForComputedKeys'; +import { Validator } from '../../'; +import { Node } from '../../../interfaces'; -export default function components ( validator, prop ) { +export default function components ( validator: Validator, prop: Node ) { if ( prop.value.type !== 'ObjectExpression' ) { validator.error( `The 'components' property must be an object literal`, prop.start ); return; @@ -10,7 +12,7 @@ export default function components ( validator, prop ) { checkForDupes( validator, prop.value.properties ); checkForComputedKeys( validator, prop.value.properties ); - prop.value.properties.forEach( component => { + prop.value.properties.forEach( ( component: Node ) => { if ( component.key.name === 'state' ) { validator.error( `Component constructors cannot be called 'state' due to technical limitations`, component.start ); } diff --git a/src/validate/js/propValidators/computed.ts b/src/validate/js/propValidators/computed.ts index 50e4a5c67f..c72ef26e73 100644 --- a/src/validate/js/propValidators/computed.ts +++ b/src/validate/js/propValidators/computed.ts @@ -1,9 +1,11 @@ import checkForDupes from '../utils/checkForDupes'; import checkForComputedKeys from '../utils/checkForComputedKeys'; +import { Validator } from '../../'; +import { Node } from '../../../interfaces'; const isFunctionExpression = new Set( [ 'FunctionExpression', 'ArrowFunctionExpression' ] ); -export default function computed ( validator, prop ) { +export default function computed ( validator: Validator, prop: Node ) { if ( prop.value.type !== 'ObjectExpression' ) { validator.error( `The 'computed' property must be an object literal`, prop.start ); return; @@ -12,7 +14,7 @@ export default function computed ( validator, prop ) { checkForDupes( validator, prop.value.properties ); checkForComputedKeys( validator, prop.value.properties ); - prop.value.properties.forEach( computation => { + prop.value.properties.forEach( ( computation: Node ) => { if ( !isFunctionExpression.has( computation.value.type ) ) { validator.error( `Computed properties can be function expressions or arrow function expressions`, computation.value.start ); return; @@ -25,7 +27,7 @@ export default function computed ( validator, prop ) { return; } - params.forEach( param => { + params.forEach( ( param: Node ) => { const valid = param.type === 'Identifier' || param.type === 'AssignmentPattern' && param.left.type === 'Identifier'; if ( !valid ) { diff --git a/src/validate/js/propValidators/data.ts b/src/validate/js/propValidators/data.ts index 5ea398136b..9727385a78 100644 --- a/src/validate/js/propValidators/data.ts +++ b/src/validate/js/propValidators/data.ts @@ -1,6 +1,9 @@ -const disallowed = new Set( [ 'Literal', 'ObjectExpression', 'ArrayExpression' ] ); +import { Validator } from '../../'; +import { Node } from '../../../interfaces'; -export default function data ( validator, prop ) { +const disallowed = new Set([ 'Literal', 'ObjectExpression', 'ArrayExpression' ]); + +export default function data ( validator: Validator, prop: Node ) { while ( prop.type === 'ParenthesizedExpression' ) prop = prop.expression; // TODO should we disallow references and expressions as well? diff --git a/src/validate/js/propValidators/events.ts b/src/validate/js/propValidators/events.ts index 2cf8ed10b7..b1c7ecb842 100644 --- a/src/validate/js/propValidators/events.ts +++ b/src/validate/js/propValidators/events.ts @@ -1,7 +1,9 @@ import checkForDupes from '../utils/checkForDupes'; import checkForComputedKeys from '../utils/checkForComputedKeys'; +import { Validator } from '../../'; +import { Node } from '../../../interfaces'; -export default function events ( validator, prop ) { +export default function events ( validator: Validator, prop: Node ) { if ( prop.value.type !== 'ObjectExpression' ) { validator.error( `The 'events' property must be an object literal`, prop.start ); return; diff --git a/src/validate/js/propValidators/helpers.ts b/src/validate/js/propValidators/helpers.ts index 09cae85189..6505eb9b5e 100644 --- a/src/validate/js/propValidators/helpers.ts +++ b/src/validate/js/propValidators/helpers.ts @@ -1,8 +1,10 @@ import checkForDupes from '../utils/checkForDupes'; import checkForComputedKeys from '../utils/checkForComputedKeys'; import { walk } from 'estree-walker'; +import { Validator } from '../../'; +import { Node } from '../../../interfaces'; -export default function helpers ( validator, prop ) { +export default function helpers ( validator: Validator, prop: Node ) { if ( prop.value.type !== 'ObjectExpression' ) { validator.error( `The 'helpers' property must be an object literal`, prop.start ); return; @@ -11,14 +13,14 @@ export default function helpers ( validator, prop ) { checkForDupes( validator, prop.value.properties ); checkForComputedKeys( validator, prop.value.properties ); - prop.value.properties.forEach( prop => { + prop.value.properties.forEach( ( prop: Node ) => { if ( !/FunctionExpression/.test( prop.value.type ) ) return; let lexicalDepth = 0; let usesArguments = false; walk( prop.value.body, { - enter ( node ) { + enter ( node: Node ) { if ( /^Function/.test( node.type ) ) { lexicalDepth += 1; } @@ -40,7 +42,7 @@ export default function helpers ( validator, prop ) { } }, - leave ( node ) { + leave ( node: Node ) { if ( /^Function/.test( node.type ) ) { lexicalDepth -= 1; } diff --git a/src/validate/js/propValidators/methods.ts b/src/validate/js/propValidators/methods.ts index bf12f921b5..144a2257ee 100644 --- a/src/validate/js/propValidators/methods.ts +++ b/src/validate/js/propValidators/methods.ts @@ -2,10 +2,12 @@ import checkForAccessors from '../utils/checkForAccessors'; import checkForDupes from '../utils/checkForDupes'; import checkForComputedKeys from '../utils/checkForComputedKeys'; import usesThisOrArguments from '../utils/usesThisOrArguments'; +import { Validator } from '../../'; +import { Node } from '../../../interfaces'; const builtin = new Set( [ 'set', 'get', 'on', 'fire', 'observe', 'destroy' ] ); -export default function methods ( validator, prop ) { +export default function methods ( validator: Validator, prop: Node ) { if ( prop.value.type !== 'ObjectExpression' ) { validator.error( `The 'methods' property must be an object literal`, prop.start ); return; @@ -15,9 +17,9 @@ export default function methods ( validator, prop ) { checkForDupes( validator, prop.value.properties ); checkForComputedKeys( validator, prop.value.properties ); - prop.value.properties.forEach( prop => { + prop.value.properties.forEach( ( prop: Node ) => { if ( builtin.has( prop.key.name ) ) { - validator.error( `Cannot overwrite built-in method '${prop.key.name}'` ); + validator.error( `Cannot overwrite built-in method '${prop.key.name}'`, prop.start ); } if ( prop.value.type === 'ArrowFunctionExpression' ) { diff --git a/src/validate/js/propValidators/namespace.ts b/src/validate/js/propValidators/namespace.ts index a551b3f3c9..5b43bf72ea 100644 --- a/src/validate/js/propValidators/namespace.ts +++ b/src/validate/js/propValidators/namespace.ts @@ -1,9 +1,11 @@ import * as namespaces from '../../../utils/namespaces'; import fuzzymatch from '../../utils/fuzzymatch'; +import { Validator } from '../../'; +import { Node } from '../../../interfaces'; const valid = new Set( namespaces.validNamespaces ); -export default function namespace ( validator, prop ) { +export default function namespace ( validator: Validator, prop: Node ) { const ns = prop.value.value; if ( prop.value.type !== 'Literal' || typeof ns !== 'string' ) { diff --git a/src/validate/js/propValidators/oncreate.ts b/src/validate/js/propValidators/oncreate.ts index 6481a53064..9ebf44a782 100644 --- a/src/validate/js/propValidators/oncreate.ts +++ b/src/validate/js/propValidators/oncreate.ts @@ -1,6 +1,8 @@ import usesThisOrArguments from '../utils/usesThisOrArguments'; +import { Validator } from '../../'; +import { Node } from '../../../interfaces'; -export default function oncreate ( validator, prop ) { +export default function oncreate ( validator: Validator, prop: Node ) { if ( prop.value.type === 'ArrowFunctionExpression' ) { if ( usesThisOrArguments( prop.value.body ) ) { validator.error( `'oncreate' should be a function expression, not an arrow function expression`, prop.start ); diff --git a/src/validate/js/propValidators/ondestroy.ts b/src/validate/js/propValidators/ondestroy.ts index c65fa78d98..62155e563f 100644 --- a/src/validate/js/propValidators/ondestroy.ts +++ b/src/validate/js/propValidators/ondestroy.ts @@ -1,6 +1,8 @@ import usesThisOrArguments from '../utils/usesThisOrArguments'; +import { Validator } from '../../'; +import { Node } from '../../../interfaces'; -export default function ondestroy ( validator, prop ) { +export default function ondestroy ( validator: Validator, prop: Node ) { if ( prop.value.type === 'ArrowFunctionExpression' ) { if ( usesThisOrArguments( prop.value.body ) ) { validator.error( `'ondestroy' should be a function expression, not an arrow function expression`, prop.start ); diff --git a/src/validate/js/propValidators/onrender.ts b/src/validate/js/propValidators/onrender.ts index d6baa00b02..0cc58c8af4 100644 --- a/src/validate/js/propValidators/onrender.ts +++ b/src/validate/js/propValidators/onrender.ts @@ -1,6 +1,8 @@ import oncreate from './oncreate'; +import { Validator } from '../../'; +import { Node } from '../../../interfaces'; -export default function onrender ( validator, prop ) { +export default function onrender ( validator: Validator, prop: Node ) { validator.warn( `'onrender' has been deprecated in favour of 'oncreate', and will cause an error in Svelte 2.x`, prop.start ); oncreate( validator, prop ); } diff --git a/src/validate/js/propValidators/onteardown.ts b/src/validate/js/propValidators/onteardown.ts index fe685d2085..6c6afef7a0 100644 --- a/src/validate/js/propValidators/onteardown.ts +++ b/src/validate/js/propValidators/onteardown.ts @@ -1,6 +1,8 @@ import ondestroy from './ondestroy'; +import { Validator } from '../../'; +import { Node } from '../../../interfaces'; -export default function onteardown ( validator, prop ) { +export default function onteardown ( validator: Validator, prop: Node ) { validator.warn( `'onteardown' has been deprecated in favour of 'ondestroy', and will cause an error in Svelte 2.x`, prop.start ); ondestroy( validator, prop ); } diff --git a/src/validate/js/propValidators/transitions.ts b/src/validate/js/propValidators/transitions.ts index e4de4e4bb2..b50302d93e 100644 --- a/src/validate/js/propValidators/transitions.ts +++ b/src/validate/js/propValidators/transitions.ts @@ -1,7 +1,9 @@ import checkForDupes from '../utils/checkForDupes'; import checkForComputedKeys from '../utils/checkForComputedKeys'; +import { Validator } from '../../'; +import { Node } from '../../../interfaces'; -export default function transitions ( validator, prop ) { +export default function transitions ( validator: Validator, prop: Node ) { if ( prop.value.type !== 'ObjectExpression' ) { validator.error( `The 'transitions' property must be an object literal`, prop.start ); return; diff --git a/src/validate/js/utils/checkForAccessors.ts b/src/validate/js/utils/checkForAccessors.ts index c6fad53dc8..45ee1eb30d 100644 --- a/src/validate/js/utils/checkForAccessors.ts +++ b/src/validate/js/utils/checkForAccessors.ts @@ -1,4 +1,7 @@ -export default function checkForAccessors ( validator, properties, label ) { +import { Validator } from '../../'; +import { Node } from '../../../interfaces'; + +export default function checkForAccessors ( validator: Validator, properties: Node[], label: string ) { properties.forEach( prop => { if ( prop.kind !== 'init' ) { validator.error( `${label} cannot use getters and setters`, prop.start ); diff --git a/src/validate/js/utils/checkForComputedKeys.ts b/src/validate/js/utils/checkForComputedKeys.ts index f4365fde9d..ca0291e2a9 100644 --- a/src/validate/js/utils/checkForComputedKeys.ts +++ b/src/validate/js/utils/checkForComputedKeys.ts @@ -1,4 +1,7 @@ -export default function checkForComputedKeys ( validator, properties ) { +import { Validator } from '../../'; +import { Node } from '../../../interfaces'; + +export default function checkForComputedKeys ( validator: Validator, properties: Node[] ) { properties.forEach( prop => { if ( prop.key.computed ) { validator.error( `Cannot use computed keys`, prop.start ); diff --git a/src/validate/js/utils/checkForDupes.ts b/src/validate/js/utils/checkForDupes.ts index b35ae79159..db16a1133f 100644 --- a/src/validate/js/utils/checkForDupes.ts +++ b/src/validate/js/utils/checkForDupes.ts @@ -1,4 +1,7 @@ -export default function checkForDupes ( validator, properties ) { +import { Validator } from '../../'; +import { Node } from '../../../interfaces'; + +export default function checkForDupes ( validator: Validator, properties: Node[] ) { const seen = new Set(); properties.forEach( prop => { diff --git a/src/validate/js/utils/usesThisOrArguments.ts b/src/validate/js/utils/usesThisOrArguments.ts index 82a0497333..2c56a57f6c 100644 --- a/src/validate/js/utils/usesThisOrArguments.ts +++ b/src/validate/js/utils/usesThisOrArguments.ts @@ -1,11 +1,12 @@ import { walk } from 'estree-walker'; import isReference from '../../../utils/isReference'; +import { Node } from '../../../interfaces'; -export default function usesThisOrArguments ( node ) { +export default function usesThisOrArguments ( node: Node ) { let result = false; walk( node, { - enter ( node ) { + enter ( node: Node, parent: Node ) { if ( result || node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration' ) { return this.skip(); } @@ -14,7 +15,7 @@ export default function usesThisOrArguments ( node ) { result = true; } - if ( node.type === 'Identifier' && isReference( node ) && node.name === 'arguments' ) { + if ( node.type === 'Identifier' && isReference( node, parent ) && node.name === 'arguments' ) { result = true; } } diff --git a/src/validate/utils/FuzzySet.ts b/src/validate/utils/FuzzySet.ts index f4b5d6502c..bed3ee1c13 100644 --- a/src/validate/utils/FuzzySet.ts +++ b/src/validate/utils/FuzzySet.ts @@ -1,4 +1,4 @@ -// adapted from https://github.com/Glench/this.js/blob/master/lib/this.js +// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js // BSD Licensed export default function FuzzySet (arr, useLevenshtein, gramSizeLower, gramSizeUpper) { diff --git a/src/validate/utils/fuzzymatch.ts b/src/validate/utils/fuzzymatch.ts index 756af6b94e..1769871df9 100644 --- a/src/validate/utils/fuzzymatch.ts +++ b/src/validate/utils/fuzzymatch.ts @@ -1,6 +1,6 @@ import FuzzySet from './FuzzySet'; -export default function fuzzymatch ( name, names ) { +export default function fuzzymatch ( name: string, names: string[] ) { const set = new FuzzySet( names ); const matches = set.get( name ); diff --git a/src/validate/utils/list.ts b/src/validate/utils/list.ts index bada8167ec..c37b6dd541 100644 --- a/src/validate/utils/list.ts +++ b/src/validate/utils/list.ts @@ -1,4 +1,4 @@ -export default function list ( items, conjunction = 'or' ) { +export default function list ( items: string[], 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