Merge pull request #1106 from sveltejs/gh-1083

Enforce valid names for computed properties
pull/1105/merge
Rich Harris 7 years ago committed by GitHub
commit d0be845190
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,9 +1,11 @@
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import { locate, Location } from 'locate-character'; import { locate, Location } from 'locate-character';
import fragment from './state/fragment'; import fragment from './state/fragment';
import { whitespace } from '../utils/patterns'; import { whitespace } from '../utils/patterns';
import { trimStart, trimEnd } from '../utils/trim'; import { trimStart, trimEnd } from '../utils/trim';
import getCodeFrame from '../utils/getCodeFrame'; import getCodeFrame from '../utils/getCodeFrame';
import reservedNames from '../utils/reservedNames'; import reservedNames from '../utils/reservedNames';
import fullCharCodeAt from '../utils/fullCharCodeAt';
import hash from './utils/hash'; import hash from './utils/hash';
import { Node, Parsed } from '../interfaces'; import { Node, Parsed } from '../interfaces';
import CompileError from '../utils/CompileError'; import CompileError from '../utils/CompileError';
@ -147,7 +149,22 @@ export class Parser {
readIdentifier() { readIdentifier() {
const start = this.index; const start = this.index;
const identifier = this.read(/[a-zA-Z_$][a-zA-Z0-9_$]*/);
let i = this.index;
const code = fullCharCodeAt(this.template, i);
if (!isIdentifierStart(code, true)) return null;
i += code <= 0xffff ? 1 : 2;
while (i < this.template.length) {
const code = fullCharCodeAt(this.template, i);
if (!isIdentifierChar(code, true)) break;
i += code <= 0xffff ? 1 : 2;
}
const identifier = this.template.slice(this.index, this.index = i);
if (reservedNames.has(identifier)) { if (reservedNames.has(identifier)) {
this.error(`'${identifier}' is a reserved word in JavaScript and cannot be used here`, start); this.error(`'${identifier}' is a reserved word in JavaScript and cannot be used here`, start);

@ -0,0 +1,10 @@
// Adapted from https://github.com/acornjs/acorn/blob/6584815dca7440e00de841d1dad152302fdd7ca5/src/tokenize.js
// Reproduced under MIT License https://github.com/acornjs/acorn/blob/master/LICENSE
export default function fullCharCodeAt(str: string, i: number): number {
let code = str.charCodeAt(i)
if (code <= 0xd7ff || code >= 0xe000) return code;
let next = str.charCodeAt(i + 1);
return (code << 10) + next - 0x35fdc00;
}

@ -0,0 +1,15 @@
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fullCharCodeAt from './fullCharCodeAt';
export default function isValidIdentifier(str: string): boolean {
let i = 0;
while (i < str.length) {
const code = fullCharCodeAt(str, i);
if (!(i === 0 ? isIdentifierStart : isIdentifierChar)(code, true)) return false;
i += code <= 0xffff ? 1 : 2;
}
return true;
}

@ -1,5 +1,8 @@
import checkForDupes from '../utils/checkForDupes'; import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys'; import checkForComputedKeys from '../utils/checkForComputedKeys';
import getName from '../../../utils/getName';
import isValidIdentifier from '../../../utils/isValidIdentifier';
import reservedNames from '../../../utils/reservedNames';
import { Validator } from '../../'; import { Validator } from '../../';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
import walkThroughTopFunctionScope from '../../../utils/walkThroughTopFunctionScope'; import walkThroughTopFunctionScope from '../../../utils/walkThroughTopFunctionScope';
@ -22,6 +25,23 @@ export default function computed(validator: Validator, prop: Node) {
checkForComputedKeys(validator, prop.value.properties); checkForComputedKeys(validator, prop.value.properties);
prop.value.properties.forEach((computation: Node) => { prop.value.properties.forEach((computation: Node) => {
const name = getName(computation.key);
if (!isValidIdentifier(name)) {
const suggestion = name.replace(/[^_$a-z0-9]/ig, '_').replace(/^\d/, '_$&');
validator.error(
`Computed property name '${name}' is invalid — must be a valid identifier such as ${suggestion}`,
computation.start
);
}
if (reservedNames.has(name)) {
validator.error(
`Computed property name '${name}' is invalid — cannot be a JavaScript reserved word`,
computation.start
);
}
if (!isFunctionExpression.has(computation.value.type)) { if (!isFunctionExpression.has(computation.value.type)) {
validator.error( validator.error(
`Computed properties can be function expressions or arrow function expressions`, `Computed properties can be function expressions or arrow function expressions`,

@ -0,0 +1,3 @@
{{#each things as 𐊧}}
<p>{{𐊧}}</p>
{{/each}}

@ -0,0 +1,46 @@
{
"hash": 795130236,
"html": {
"start": 0,
"end": 47,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 47,
"type": "EachBlock",
"expression": {
"type": "Identifier",
"start": 8,
"end": 14,
"name": "things"
},
"children": [
{
"start": 24,
"end": 37,
"type": "Element",
"name": "p",
"attributes": [],
"children": [
{
"start": 27,
"end": 33,
"type": "MustacheTag",
"expression": {
"type": "Identifier",
"start": 29,
"end": 31,
"name": "𐊧"
}
}
]
}
],
"context": "𐊧"
}
]
},
"css": null,
"js": null
}

@ -0,0 +1,9 @@
[{
"message":
"Computed property name 'new' is invalid — cannot be a JavaScript reserved word",
"loc": {
"line": 9,
"column": 3
},
"pos": 87
}]

@ -0,0 +1,12 @@
<script>
export default {
data() {
return {
a: 1
};
},
computed: {
new: a => a * 2
}
};
</script>

@ -0,0 +1,8 @@
[{
"message": "Computed property name 'with-hyphen' is invalid — must be a valid identifier such as with_hyphen",
"loc": {
"line": 9,
"column": 3
},
"pos": 87
}]

@ -0,0 +1,12 @@
<script>
export default {
data() {
return {
a: 1
};
},
computed: {
"with-hyphen": a => a * 2
}
};
</script>
Loading…
Cancel
Save