Skip to content

Commit d4d7f6c

Browse files
authored
Merge pull request #473 from sveltejs/gh-166
More helpful validation
2 parents ef630b1 + cc722f8 commit d4d7f6c

File tree

20 files changed

+273
-29
lines changed

20 files changed

+273
-29
lines changed

src/utils/namespaces.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,9 @@ export const xlink = 'http://www.w3.org/1999/xlink';
55
export const xml = 'http://www.w3.org/XML/1998/namespace';
66
export const xmlns = 'http://www.w3.org/2000/xmlns';
77

8+
export const validNamespaces = [
9+
'html', 'mathml', 'svg', 'xlink', 'xml', 'xmlns',
10+
html, mathml, svg, xlink, xml, xmlns
11+
];
12+
813
export default { html, mathml, svg, xlink, xml, xmlns };

src/validate/html/index.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import * as namespaces from '../../utils/namespaces.js';
2+
import flattenReference from '../../utils/flattenReference.js';
23

34
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)$/;
45

6+
function list ( items, conjunction = 'or' ) {
7+
if ( items.length === 1 ) return items[0];
8+
return `${items.slice( 0, -1 ).join( ', ' )} ${conjunction} ${items[ items.length - 1 ]}`;
9+
}
10+
511
export default function validateHtml ( validator, html ) {
612
let elementDepth = 0;
713

@@ -12,13 +18,37 @@ export default function validateHtml ( validator, html ) {
1218
}
1319

1420
elementDepth += 1;
21+
22+
node.attributes.forEach( attribute => {
23+
if ( attribute.type === 'EventHandler' ) {
24+
const { callee, start, type } = attribute.expression;
25+
26+
if ( type !== 'CallExpression' ) {
27+
validator.error( `Expected a call expression`, start );
28+
}
29+
30+
const { name } = flattenReference( callee );
31+
32+
if ( name === 'this' || name === 'event' ) return;
33+
if ( callee.type === 'Identifier' && callee.name === 'set' || callee.name === 'fire' || callee.name in validator.methods ) return;
34+
35+
const validCallees = list( [ 'this.*', 'event.*', 'set', 'fire' ].concat( Object.keys( validator.methods ) ) );
36+
let message = `'${validator.source.slice( callee.start, callee.end )}' is an invalid callee (should be one of ${validCallees})`;
37+
38+
if ( callee.type === 'Identifier' && callee.name in validator.helpers ) {
39+
message += `. '${callee.name}' exists on 'helpers', did you put it in the wrong place?`;
40+
}
41+
42+
validator.error( message, start );
43+
}
44+
});
1545
}
1646

1747
if ( node.children ) {
1848
node.children.forEach( visit );
1949
}
2050

21-
if (node.else ) {
51+
if ( node.else ) {
2252
visit( node.else );
2353
}
2454

src/validate/index.js

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default function validate ( parsed, source, { onerror, onwarn, name, file
1818

1919
error.toString = () => `${error.message} (${error.loc.line}:${error.loc.column})\n${error.frame}`;
2020

21-
onerror( error );
21+
throw error;
2222
},
2323

2424
warn: ( message, pos ) => {
@@ -36,28 +36,42 @@ export default function validate ( parsed, source, { onerror, onwarn, name, file
3636
});
3737
},
3838

39-
namespace: null
39+
source,
40+
41+
namespace: null,
42+
defaultExport: null,
43+
properties: {},
44+
methods: {},
45+
helpers: {}
4046
};
4147

42-
if ( name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test( name ) ) {
43-
const error = new Error( `options.name must be a valid identifier` );
44-
onerror( error );
45-
}
48+
try {
49+
if ( name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test( name ) ) {
50+
const error = new Error( `options.name must be a valid identifier` );
51+
throw error;
52+
}
4653

47-
if ( name && !/^[A-Z]/.test( name ) ) {
48-
const message = `options.name should be capitalised`;
49-
onwarn({
50-
message,
51-
filename,
52-
toString: () => message
53-
});
54-
}
54+
if ( name && !/^[A-Z]/.test( name ) ) {
55+
const message = `options.name should be capitalised`;
56+
onwarn({
57+
message,
58+
filename,
59+
toString: () => message
60+
});
61+
}
5562

56-
if ( parsed.js ) {
57-
validateJs( validator, parsed.js );
58-
}
63+
if ( parsed.js ) {
64+
validateJs( validator, parsed.js );
65+
}
5966

60-
if ( parsed.html ) {
61-
validateHtml( validator, parsed.html );
67+
if ( parsed.html ) {
68+
validateHtml( validator, parsed.html );
69+
}
70+
} catch ( err ) {
71+
if ( onerror ) {
72+
onerror( err );
73+
} else {
74+
throw err;
75+
}
6276
}
6377
}

src/validate/js/index.js

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,19 @@ export default function validateJs ( validator, js ) {
2323
checkForComputedKeys( validator, node.declaration.properties );
2424
checkForDupes( validator, node.declaration.properties );
2525

26-
const templateProperties = {};
26+
const props = validator.properties;
2727

2828
node.declaration.properties.forEach( prop => {
29-
templateProperties[ prop.key.name ] = prop;
29+
props[ prop.key.name ] = prop;
3030
});
3131

3232
// Remove these checks in version 2
33-
if ( templateProperties.oncreate && templateProperties.onrender ) {
34-
validator.error( 'Cannot have both oncreate and onrender', templateProperties.onrender.start );
33+
if ( props.oncreate && props.onrender ) {
34+
validator.error( 'Cannot have both oncreate and onrender', props.onrender.start );
3535
}
3636

37-
if ( templateProperties.ondestroy && templateProperties.onteardown ) {
38-
validator.error( 'Cannot have both ondestroy and onteardown', templateProperties.onteardown.start );
37+
if ( props.ondestroy && props.onteardown ) {
38+
validator.error( 'Cannot have both ondestroy and onteardown', props.onteardown.start );
3939
}
4040

4141
// ensure all exported props are valid
@@ -56,8 +56,8 @@ export default function validateJs ( validator, js ) {
5656
}
5757
});
5858

59-
if ( templateProperties.namespace ) {
60-
const ns = templateProperties.namespace.value.value;
59+
if ( props.namespace ) {
60+
const ns = props.namespace.value.value;
6161
validator.namespace = namespaces[ ns ] || ns;
6262
}
6363

@@ -72,4 +72,12 @@ export default function validateJs ( validator, js ) {
7272
});
7373
}
7474
});
75+
76+
[ 'methods', 'helpers' ].forEach( key => {
77+
if ( validator.properties[ key ] ) {
78+
validator.properties[ key ].value.properties.forEach( prop => {
79+
validator[ key ][ prop.key.name ] = prop.value;
80+
});
81+
}
82+
});
7583
}

src/validate/js/propValidators/helpers.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import checkForDupes from '../utils/checkForDupes.js';
22
import checkForComputedKeys from '../utils/checkForComputedKeys.js';
3+
import { walk } from 'estree-walker';
34

45
export default function helpers ( validator, prop ) {
56
if ( prop.value.type !== 'ObjectExpression' ) {
@@ -9,4 +10,45 @@ export default function helpers ( validator, prop ) {
910

1011
checkForDupes( validator, prop.value.properties );
1112
checkForComputedKeys( validator, prop.value.properties );
13+
14+
prop.value.properties.forEach( prop => {
15+
if ( !/FunctionExpression/.test( prop.value.type ) ) return;
16+
17+
let lexicalDepth = 0;
18+
let usesArguments = false;
19+
20+
walk( prop.value.body, {
21+
enter ( node ) {
22+
if ( /^Function/.test( node.type ) ) {
23+
lexicalDepth += 1;
24+
}
25+
26+
else if ( lexicalDepth === 0 ) {
27+
// handle special case that's caused some people confusion — using `this.get(...)` instead of passing argument
28+
// TODO do the same thing for computed values?
29+
if ( node.type === 'CallExpression' && node.callee.type === 'MemberExpression' && node.callee.object.type === 'ThisExpression' && node.callee.property.name === 'get' && !node.callee.property.computed ) {
30+
validator.error( `Cannot use this.get(...) — it must be passed into the helper function as an argument`, node.start );
31+
}
32+
33+
if ( node.type === 'ThisExpression' ) {
34+
validator.error( `Helpers should be pure functions — they do not have access to the component instance and cannot use 'this'. Did you mean to put this in 'methods'?`, node.start );
35+
}
36+
37+
else if ( node.type === 'Identifier' && node.name === 'arguments' ) {
38+
usesArguments = true;
39+
}
40+
}
41+
},
42+
43+
leave ( node ) {
44+
if ( /^Function/.test( node.type ) ) {
45+
lexicalDepth -= 1;
46+
}
47+
}
48+
});
49+
50+
if ( prop.value.params.length === 0 && !usesArguments ) {
51+
validator.warn( `Helpers should be pure functions, with at least one argument`, prop.start );
52+
}
53+
});
1254
}
Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
1+
import * as namespaces from '../../../utils/namespaces.js';
2+
import FuzzySet from '../utils/FuzzySet.js';
3+
4+
const fuzzySet = new FuzzySet( namespaces.validNamespaces );
5+
const valid = new Set( namespaces.validNamespaces );
6+
17
export default function namespace ( validator, prop ) {
2-
if ( prop.value.type !== 'Literal' || typeof prop.value.value !== 'string' ) {
8+
const ns = prop.value.value;
9+
10+
if ( prop.value.type !== 'Literal' || typeof ns !== 'string' ) {
311
validator.error( `The 'namespace' property must be a string literal representing a valid namespace`, prop.start );
412
}
13+
14+
if ( !valid.has( ns ) ) {
15+
const matches = fuzzySet.get( ns );
16+
if ( matches && matches[0] && matches[0][0] > 0.7 ) {
17+
validator.error( `Invalid namespace '${ns}' (did you mean '${matches[0][1]}'?)`, prop.start );
18+
} else {
19+
validator.error( `Invalid namespace '${ns}'`, prop.start );
20+
}
21+
}
522
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{{foo()}}
2+
3+
<script>
4+
export default {
5+
helpers: {
6+
foo () {
7+
return Math.random();
8+
}
9+
}
10+
}
11+
</script>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[{
2+
"message": "Helpers should be pure functions, with at least one argument",
3+
"pos": 54,
4+
"loc": {
5+
"line": 6,
6+
"column": 3
7+
}
8+
}]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[{
2+
"message": "Helpers should be pure functions — they do not have access to the component instance and cannot use 'this'. Did you mean to put this in 'methods'?",
3+
"pos": 95,
4+
"loc": {
5+
"line": 7,
6+
"column": 4
7+
}
8+
}]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<button on:click='foo()'>foo</button>
2+
3+
<script>
4+
export default {
5+
helpers: {
6+
foo () {
7+
this.set({ foo: true });
8+
}
9+
}
10+
}
11+
</script>

0 commit comments

Comments
 (0)