Skip to content

Commit 0698525

Browse files
committed
basic validation
1 parent c1d230d commit 0698525

File tree

34 files changed

+704
-40
lines changed

34 files changed

+704
-40
lines changed

compiler/index.js

+19-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
import parse from './parse/index.js';
2+
import validate from './validate/index.js';
23
import generate from './generate/index.js';
34

4-
export function compile ( template, options = {} ) {
5-
const parsed = parse( template, options );
6-
// TODO validate template
7-
const generated = generate( parsed, template, options );
5+
export function compile ( source, options = {} ) {
6+
const parsed = parse( source, options );
87

9-
return generated;
8+
const { errors, warnings } = validate( parsed, source, options );
9+
10+
if ( errors.length ) {
11+
// TODO optionally show all errors?
12+
throw errors[0];
13+
}
14+
15+
if ( warnings.length ) {
16+
console.warn( `Svelte: ${warnings.length} ${warnings.length === 1 ? 'error' : 'errors'} in ${options.filename || 'template'}:` );
17+
warnings.forEach( warning => {
18+
console.warn( `(${warning.loc.line}:${warning.loc.column}) – ${warning.message}` );
19+
});
20+
}
21+
22+
return generate( parsed, source, options );
1023
}
1124

12-
export { parse };
25+
export { parse, validate };

compiler/parse/index.js

+2-26
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,13 @@ import { locate } from 'locate-character';
22
import fragment from './state/fragment.js';
33
import { whitespace } from './patterns.js';
44
import { trimStart, trimEnd } from './utils/trim.js';
5-
import spaces from '../utils/spaces.js';
5+
import getCodeFrame from '../utils/getCodeFrame.js';
66
import hash from './utils/hash.js';
77

8-
function tabsToSpaces ( str ) {
9-
return str.replace( /^\t+/, match => match.split( '\t' ).join( ' ' ) );
10-
}
11-
128
function ParseError ( message, template, index ) {
139
const { line, column } = locate( template, index );
14-
const lines = template.split( '\n' );
15-
16-
const frameStart = Math.max( 0, line - 2 );
17-
const frameEnd = Math.min( line + 3, lines.length );
18-
19-
const digits = String( frameEnd + 1 ).length;
20-
const frame = lines
21-
.slice( frameStart, frameEnd )
22-
.map( ( str, i ) => {
23-
const isErrorLine = frameStart + i === line;
24-
25-
let lineNum = String( i + frameStart + 1 );
26-
while ( lineNum.length < digits ) lineNum = ` ${lineNum}`;
27-
28-
if ( isErrorLine ) {
29-
const indicator = spaces( digits + 2 + tabsToSpaces( str.slice( 0, column ) ).length ) + '^';
30-
return `${lineNum}: ${tabsToSpaces( str )}\n${indicator}`;
31-
}
3210

33-
return `${lineNum}: ${tabsToSpaces( str )}`;
34-
})
35-
.join( '\n' );
11+
const frame = getCodeFrame( template, line, column );
3612

3713
this.name = 'ParseError';
3814
this.message = `${message} (${line + 1}:${column})\n${frame}`;

compiler/utils/getCodeFrame.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import spaces from './spaces.js';
2+
3+
function tabsToSpaces ( str ) {
4+
return str.replace( /^\t+/, match => match.split( '\t' ).join( ' ' ) );
5+
}
6+
7+
export default function getCodeFrame ( source, line, column ) {
8+
const lines = source.split( '\n' );
9+
10+
const frameStart = Math.max( 0, line - 2 );
11+
const frameEnd = Math.min( line + 3, lines.length );
12+
13+
const digits = String( frameEnd + 1 ).length;
14+
15+
return lines
16+
.slice( frameStart, frameEnd )
17+
.map( ( str, i ) => {
18+
const isErrorLine = frameStart + i === line;
19+
20+
let lineNum = String( i + frameStart + 1 );
21+
while ( lineNum.length < digits ) lineNum = ` ${lineNum}`;
22+
23+
if ( isErrorLine ) {
24+
const indicator = spaces( digits + 2 + tabsToSpaces( str.slice( 0, column ) ).length ) + '^';
25+
return `${lineNum}: ${tabsToSpaces( str )}\n${indicator}`;
26+
}
27+
28+
return `${lineNum}: ${tabsToSpaces( str )}`;
29+
})
30+
.join( '\n' );
31+
}

compiler/validate/index.js

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import validateJs from './js/index.js';
2+
import { getLocator } from 'locate-character';
3+
4+
export default function validate ( parsed, source ) {
5+
const locator = getLocator( source );
6+
7+
const validator = {
8+
error: ( message, pos ) => {
9+
const { line, column } = locator( pos );
10+
11+
validator.errors.push({
12+
message,
13+
pos,
14+
loc: { line: line + 1, column }
15+
});
16+
},
17+
18+
warn: ( message, pos ) => {
19+
const { line, column } = locator( pos );
20+
21+
validator.warnings.push({
22+
message,
23+
pos,
24+
loc: { line: line + 1, column }
25+
});
26+
},
27+
28+
templateProperties: {},
29+
30+
errors: [],
31+
warnings: []
32+
};
33+
34+
if ( parsed.js ) {
35+
validateJs( validator, parsed.js, source );
36+
}
37+
38+
return {
39+
errors: validator.errors,
40+
warnings: validator.warnings
41+
};
42+
}

compiler/validate/js/index.js

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import propValidators from './propValidators/index.js';
2+
import FuzzySet from './utils/FuzzySet.js';
3+
import checkForDupes from './utils/checkForDupes.js';
4+
import checkForComputedKeys from './utils/checkForComputedKeys.js';
5+
6+
const validPropList = Object.keys( propValidators );
7+
8+
const fuzzySet = new FuzzySet( validPropList );
9+
10+
export default function validateJs ( validator, js, source ) {
11+
js.content.body.forEach( node => {
12+
// check there are no named exports
13+
if ( node.type === 'ExportNamedDeclaration' ) {
14+
validator.error( `A component can only have a default export`, node.start );
15+
}
16+
17+
if ( node.type === 'ExportDefaultDeclaration' ) {
18+
if ( validator.defaultExport ) {
19+
validator.error( `Duplicate default export`, node.start );
20+
}
21+
22+
validator.defaultExport = node;
23+
}
24+
});
25+
26+
// ensure all exported props are valid
27+
if ( validator.defaultExport ) {
28+
checkForComputedKeys( validator, validator.defaultExport.declaration.properties );
29+
checkForDupes( validator, validator.defaultExport.declaration.properties );
30+
31+
validator.defaultExport.declaration.properties.forEach( prop => {
32+
validator.templateProperties[ prop.key.value ] = prop;
33+
});
34+
35+
validator.defaultExport.declaration.properties.forEach( prop => {
36+
const propValidator = propValidators[ prop.key.name ];
37+
38+
if ( propValidator ) {
39+
propValidator( validator, prop );
40+
} else {
41+
const matches = fuzzySet.get( prop.key.name );
42+
if ( matches && matches[0] && matches[0][0] > 0.7 ) {
43+
validator.error( `Unexpected property '${prop.key.name}' (did you mean '${matches[0][1]}'?)`, prop.start );
44+
} else if ( /FunctionExpression/.test( prop.value.type ) ) {
45+
validator.error( `Unexpected property '${prop.key.name}' (did you mean to include it in 'methods'?)`, prop.start );
46+
} else {
47+
validator.error( `Unexpected property '${prop.key.name}'`, prop.start );
48+
}
49+
}
50+
});
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function components ( validator, prop ) {
2+
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import checkForDupes from '../utils/checkForDupes.js';
2+
import checkForComputedKeys from '../utils/checkForComputedKeys.js';
3+
4+
const isFunctionExpression = {
5+
FunctionExpression: true,
6+
ArrowFunctionExpression: true
7+
};
8+
9+
export default function computed ( validator, prop ) {
10+
if ( prop.value.type !== 'ObjectExpression' ) {
11+
validator.error( `The 'computed' property must be an object literal`, prop.start );
12+
return;
13+
}
14+
15+
checkForDupes( validator, prop.value.properties );
16+
checkForComputedKeys( validator, prop.value.properties );
17+
18+
prop.value.properties.forEach( computation => {
19+
if ( !isFunctionExpression[ computation.value.type ] ) {
20+
validator.error( `Computed properties can be function expressions or arrow function expressions`, computation.value.start );
21+
return;
22+
}
23+
24+
computation.value.params.forEach( param => {
25+
const valid = param.type === 'Identifier' || param.type === 'AssignmentPattern' && param.left.type === 'Identifier';
26+
27+
if ( !valid ) {
28+
validator.error( `Computed properties cannot use destructuring in function parameters`, param.start );
29+
}
30+
});
31+
});
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function data ( validator, prop ) {
2+
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function events ( validator, prop ) {
2+
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function helpers ( validator, prop ) {
2+
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import data from './data.js';
2+
import computed from './computed.js';
3+
import onrender from './onrender.js';
4+
import onteardown from './onteardown.js';
5+
import helpers from './helpers.js';
6+
import methods from './methods.js';
7+
import components from './components.js';
8+
import events from './events.js';
9+
10+
export default {
11+
data,
12+
computed,
13+
onrender,
14+
onteardown,
15+
helpers,
16+
methods,
17+
components,
18+
events
19+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function methods ( validator, prop ) {
2+
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function onrender ( validator, prop ) {
2+
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function onteardown ( validator, prop ) {
2+
3+
}

0 commit comments

Comments
 (0)