typescriptify validator

pull/573/head
Rich-Harris 7 years ago
parent 7c3fca57cf
commit 48384b846c

@ -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}`;
}
}

@ -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_$]*/;

@ -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"'=<>\/`]/;

@ -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}`;
}
}

@ -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 );
}

@ -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<string, Node>;
methods: Map<string, Node>;
helpers: Map<string, Node>;
transitions: Map<string, Node>;
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 );
}

@ -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 ) {

@ -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 );
}

@ -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 ) {

@ -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?

@ -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;

@ -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;
}

@ -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' ) {

@ -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' ) {

@ -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 );

@ -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 );

@ -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 );
}

@ -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 );
}

@ -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;

@ -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 );

@ -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 );

@ -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 => {

@ -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;
}
}

@ -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) {

@ -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 );

@ -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 ]}`;
}
Loading…
Cancel
Save