diff --git a/src/parse/index.ts b/src/parse/index.ts index 6711e9929f..03c7cb95e5 100644 --- a/src/parse/index.ts +++ b/src/parse/index.ts @@ -1,9 +1,11 @@ +import { isIdentifierStart, isIdentifierChar } from 'acorn'; import { locate, Location } from 'locate-character'; import fragment from './state/fragment'; import { whitespace } from '../utils/patterns'; import { trimStart, trimEnd } from '../utils/trim'; import getCodeFrame from '../utils/getCodeFrame'; import reservedNames from '../utils/reservedNames'; +import fullCharCodeAt from '../utils/fullCharCodeAt'; import hash from './utils/hash'; import { Node, Parsed } from '../interfaces'; import CompileError from '../utils/CompileError'; @@ -147,7 +149,22 @@ export class Parser { readIdentifier() { 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)) { this.error(`'${identifier}' is a reserved word in JavaScript and cannot be used here`, start); diff --git a/src/utils/fullCharCodeAt.ts b/src/utils/fullCharCodeAt.ts new file mode 100644 index 0000000000..034da9258b --- /dev/null +++ b/src/utils/fullCharCodeAt.ts @@ -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; +} \ No newline at end of file diff --git a/src/utils/isValidIdentifier.ts b/src/utils/isValidIdentifier.ts new file mode 100644 index 0000000000..20ceeb4bbf --- /dev/null +++ b/src/utils/isValidIdentifier.ts @@ -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; +} \ No newline at end of file diff --git a/src/validate/js/propValidators/computed.ts b/src/validate/js/propValidators/computed.ts index 644043ddb1..94a7212881 100644 --- a/src/validate/js/propValidators/computed.ts +++ b/src/validate/js/propValidators/computed.ts @@ -1,5 +1,8 @@ import checkForDupes from '../utils/checkForDupes'; import checkForComputedKeys from '../utils/checkForComputedKeys'; +import getName from '../../../utils/getName'; +import isValidIdentifier from '../../../utils/isValidIdentifier'; +import reservedNames from '../../../utils/reservedNames'; import { Validator } from '../../'; import { Node } from '../../../interfaces'; import walkThroughTopFunctionScope from '../../../utils/walkThroughTopFunctionScope'; @@ -22,6 +25,23 @@ export default function computed(validator: Validator, prop: Node) { checkForComputedKeys(validator, prop.value.properties); 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)) { validator.error( `Computed properties can be function expressions or arrow function expressions`, diff --git a/test/parser/samples/unusual-identifier/input.html b/test/parser/samples/unusual-identifier/input.html new file mode 100644 index 0000000000..71c3f4a12f --- /dev/null +++ b/test/parser/samples/unusual-identifier/input.html @@ -0,0 +1,3 @@ +{{#each things as 𐊧}} +

{{𐊧}}

+{{/each}} \ No newline at end of file diff --git a/test/parser/samples/unusual-identifier/output.json b/test/parser/samples/unusual-identifier/output.json new file mode 100644 index 0000000000..6434526df5 --- /dev/null +++ b/test/parser/samples/unusual-identifier/output.json @@ -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 +} \ No newline at end of file diff --git a/test/validator/samples/properties-computed-cannot-be-reserved/errors.json b/test/validator/samples/properties-computed-cannot-be-reserved/errors.json new file mode 100644 index 0000000000..39c4039743 --- /dev/null +++ b/test/validator/samples/properties-computed-cannot-be-reserved/errors.json @@ -0,0 +1,9 @@ +[{ + "message": + "Computed property name 'new' is invalid — cannot be a JavaScript reserved word", + "loc": { + "line": 9, + "column": 3 + }, + "pos": 87 +}] diff --git a/test/validator/samples/properties-computed-cannot-be-reserved/input.html b/test/validator/samples/properties-computed-cannot-be-reserved/input.html new file mode 100644 index 0000000000..06bd2bf694 --- /dev/null +++ b/test/validator/samples/properties-computed-cannot-be-reserved/input.html @@ -0,0 +1,12 @@ + diff --git a/test/validator/samples/properties-computed-must-be-valid-function-names/errors.json b/test/validator/samples/properties-computed-must-be-valid-function-names/errors.json new file mode 100644 index 0000000000..ebaac56a8f --- /dev/null +++ b/test/validator/samples/properties-computed-must-be-valid-function-names/errors.json @@ -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 +}] diff --git a/test/validator/samples/properties-computed-must-be-valid-function-names/input.html b/test/validator/samples/properties-computed-must-be-valid-function-names/input.html new file mode 100644 index 0000000000..1c9173f523 --- /dev/null +++ b/test/validator/samples/properties-computed-must-be-valid-function-names/input.html @@ -0,0 +1,12 @@ +