diff --git a/History.md b/History.md index 45aecaf1b6..f6de08445e 100644 --- a/History.md +++ b/History.md @@ -1,5 +1,49 @@ ## v.NEXT +* Although Meteor does not recompile packages installed in `node_modules` + by default, compilation of specific npm packages (for example, to + support older browsers that the package author neglected) can now be + enabled in one of two ways: + + * Clone the package repository into your application's `imports` + directory, make any modifications necessary, then use `npm install` to + link `the-package` into `node_modules`: + ```sh + meteor npm install imports/the-package + ``` + Meteor will compile the contents of the package exposed via + `imports/the-package`, and this compiled code will be used when you + import `the-package` in any of the usual ways: + ```js + import stuff from "the-package" + require("the-package") === require("/imports/the-package") + import("the-package").then(...) + ``` + This reuse of compiled code is the critical new feature that was added + in Meteor 1.6.2. + + * Install the package normally with `meteor npm install the-package`, + then create a symbolic link *to* the installed package elsewhere in + your application, outside of `node_modules`: + ```sh + meteor npm install the-package + cd imports + ln -s ../node_modules/the-package . + ``` + Again, Meteor will compile the contents of the package because they + are exposed outside of `node_modules`, and the compiled code will be + used whenever `the-package` is imported from `node_modules`. + + > Note: this technique also works if you create symbolic links to + individual files, rather than linking the entire package directory. + + In both cases, Meteor will compile the exposed code as if it was part of + your application, using whatever compiler plugins you have installed. + You can influence this compilation using `.babelrc` files or any other + techniques you would normally use to configure compilation of + application code. [PR #9771](https://github.com/meteor/meteor/pull/9771) + [Feature #6](https://github.com/meteor/meteor-feature-requests/issues/6) + * Applications may now specify client and server entry point modules in a newly-supported `"meteor"` section of `package.json`: ```js diff --git a/packages/modules-runtime/.npm/package/npm-shrinkwrap.json b/packages/modules-runtime/.npm/package/npm-shrinkwrap.json index cab824928f..d79cb72b6b 100644 --- a/packages/modules-runtime/.npm/package/npm-shrinkwrap.json +++ b/packages/modules-runtime/.npm/package/npm-shrinkwrap.json @@ -2,9 +2,9 @@ "lockfileVersion": 1, "dependencies": { "install": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/install/-/install-0.10.4.tgz", - "integrity": "sha512-+IRyOastuPmLVx9zlVXJoKErSqz1Ma5at9A7S8yfsj3W+Kg95faPoh3bPDtMrZ/grz4PRmXzrswmlzfLlYyLOw==" + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/install/-/install-0.11.0.tgz", + "integrity": "sha512-30UqvWKr/59SStU2/bRye4wT1S3mzjwa0oV+BxusB0neGqhxUrwLlnXDbt6QtIfBxCNWFqg+ARnGNjFj8XuV5A==" } } } diff --git a/packages/modules-runtime/package.js b/packages/modules-runtime/package.js index b0bbb12abe..96b9a42582 100644 --- a/packages/modules-runtime/package.js +++ b/packages/modules-runtime/package.js @@ -1,13 +1,13 @@ Package.describe({ name: "modules-runtime", - version: "0.9.2", + version: "0.10.0", summary: "CommonJS module system", git: "https://github.com/benjamn/install", documentation: "README.md" }); Npm.depends({ - install: "0.10.4" + install: "0.11.0" }); Package.onUse(function(api) { diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 2ae436df5c..5fef22f37f 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -582,7 +582,8 @@ main.registerCommand({ return contents; } }, - ignore: [/^local$/] + ignore: [/^local$/], + preserveSymlinks: true, }); } catch (err) { Console.error("Could not create package: " + err.message); @@ -750,7 +751,8 @@ main.registerCommand({ return contents; } }, - ignore: toIgnore + ignore: toIgnore, + preserveSymlinks: true, }); // We are actually working with a new meteor project at this point, so diff --git a/tools/fs/files.js b/tools/fs/files.js index 260b531ce4..f5286fc165 100644 --- a/tools/fs/files.js +++ b/tools/fs/files.js @@ -297,6 +297,15 @@ function statOrNull(path, preserveSymlinks) { } } +export function realpathOrNull(path) { + try { + return files.realpath(path); + } catch (e) { + if (e.code !== "ENOENT") throw e; + return null; + } +} + files.rm_recursive_async = (path) => { return new Promise((resolve, reject) => { rimraf(files.convertToOSPath(path), err => err @@ -495,7 +504,7 @@ files.cp_r = function(from, to, options = {}) { files.mkdir_p(files.pathDirname(to)); if (stat.isSymbolicLink()) { - files.symlink(files.readlink(from), to); + symlinkWithOverwrite(files.readlink(from), to); } else { // Create the file as readable and writable by everyone, and @@ -517,6 +526,28 @@ files.cp_r = function(from, to, options = {}) { } }; +// create a symlink, overwriting the target link, file, or directory +// if it exists +export function symlinkWithOverwrite(source, target) { + try { + files.symlink(source, target); + } catch (e) { + if (e.code === "EEXIST") { + // overwrite existing link, file, or directory + files.rm_recursive(target); + files.symlink(source, target); + } else if (e.code === "EPERM" && + process.platform === "win32") { + files.rm_recursive(target); + // This will work only if source refers to a directory, but that's a + // chance worth taking. + files.symlink(source, target, "junction"); + } else { + throw e; + } + } +} + /** * Get every path in a directory recursively, treating symlinks as files * @param {String} dir The directory to walk, either relative to options.cwd or completely absolute @@ -1003,7 +1034,9 @@ files.renameDirAlmostAtomically = // limitations, we'll resort to copying. if (forceCopy) { files.rm_recursive(toDir); - files.cp_r(fromDir, toDir); + files.cp_r(fromDir, toDir, { + preserveSymlinks: true, + }); } // ... and take out the trash. @@ -1750,7 +1783,7 @@ if (files.isWindowsLikeFilesystem()) { } if (! success) { - files.cp_r(from, to); + files.cp_r(from, to, { preserveSymlinks: true }); files.rm_recursive(from); } }; diff --git a/tools/isobuild/builder.js b/tools/isobuild/builder.js index fdb5973fba..b9566acf2b 100644 --- a/tools/isobuild/builder.js +++ b/tools/isobuild/builder.js @@ -1,6 +1,8 @@ import assert from "assert"; import {WatchSet, readAndWatchFile, sha1} from '../fs/watch.js'; -import files from '../fs/files.js'; +import files, { + symlinkWithOverwrite, +} from '../fs/files.js'; import NpmDiscards from './npm-discards.js'; import {Profile} from '../tool-env/profile.js'; import { @@ -393,7 +395,10 @@ Previous builder: ${previousBuilder.outputPath}, this builder: ${outputPath}` // A version of copyDirectory that works better for copying node_modules // directories when symlinks are involved. copyNodeModulesDirectory(options) { - assert.strictEqual(files.pathBasename(options.from), "node_modules"); + // Although the options.from directory should probably be a + // node_modules directory, the only essential precondition here is + // that the destination directory is a node_modules directory. + // assert.strictEqual(files.pathBasename(options.from), "node_modules"); assert.strictEqual(files.pathBasename(options.to), "node_modules"); if (options.symlink) { @@ -805,28 +810,6 @@ function symlinkIfPossible(source, target) { } } -// create a symlink, overwriting the target link, file, or directory -// if it exists -function symlinkWithOverwrite(source, target) { - try { - files.symlink(source, target); - } catch (e) { - if (e.code === "EEXIST") { - // overwrite existing link, file, or directory - files.rm_recursive(target); - files.symlink(source, target); - } else if (e.code === "EPERM" && - process.platform === "win32") { - files.rm_recursive(target); - // This will work only if source refers to a directory, but that's a - // chance worth taking. - files.symlink(source, target, "junction"); - } else { - throw e; - } - } -} - // Wrap slow methods into Profiler calls const slowBuilderMethods = [ "_ensureDirectory", diff --git a/tools/isobuild/bundler.js b/tools/isobuild/bundler.js index 6b84a4a5e5..2cd07f968f 100644 --- a/tools/isobuild/bundler.js +++ b/tools/isobuild/bundler.js @@ -482,7 +482,7 @@ export class NodeModulesDirectory { return true; } - const real = realpathOrNull(path); + const real = files.realpathOrNull(path); if (typeof real === "string" && real !== path) { // If node_modules/.bin/command is a symlink, determine the @@ -522,15 +522,6 @@ export class NodeModulesDirectory { } } -function realpathOrNull(path) { - try { - return files.realpath(path); - } catch (e) { - if (e.code !== "ENOENT") throw e; - return null; - } -} - /////////////////////////////////////////////////////////////////////////////// // File /////////////////////////////////////////////////////////////////////////////// diff --git a/tools/isobuild/import-scanner.js b/tools/isobuild/import-scanner.js index 57e8f6642b..b1163a8fda 100644 --- a/tools/isobuild/import-scanner.js +++ b/tools/isobuild/import-scanner.js @@ -18,9 +18,11 @@ import { pathNormalize, pathBasename, pathExtname, + pathDirname, pathIsAbsolute, convertToOSPath, convertToPosixPath, + realpathOrNull, } from "../fs/files.js"; const { @@ -32,6 +34,7 @@ const { import { optimisticReadFile, optimisticStatOrNull, + optimisticLStatOrNull, optimisticHashOrNull, shouldWatch, } from "../fs/optimistic.js"; @@ -134,6 +137,18 @@ const IMPORT_SCANNER_CACHE = new LRU({ } }); +// Stub used for entry point modules within node_modules directories on +// the server. These stub modules delegate to native Node evaluation by +// calling module.useNode() immediately, but it's important that we have +// something to include in the bundle so that parent modules have +// something to resolve. +const useNodeStub = { + dataString: "module.useNode();", + deps: Object.create(null), +}; +useNodeStub.data = Buffer.from(useNodeStub.dataString, "utf8"); +useNodeStub.hash = sha1(useNodeStub.data); + export default class ImportScanner { constructor({ name, @@ -151,6 +166,8 @@ export default class ImportScanner { this.nodeModulesPaths = nodeModulesPaths; this.watchSet = watchSet; this.absPathToOutputIndex = Object.create(null); + this.realPathToFiles = Object.create(null); + this.realPathCache = Object.create(null); this.allMissingModules = Object.create(null); this.outputFiles = []; @@ -193,10 +210,12 @@ export default class ImportScanner { return file; } - absPath = absPath.toLowerCase(); - const old = this.absPathToOutputIndex[absPath]; + const absLowerPath = absPath.toLowerCase(); + + if (has(this.absPathToOutputIndex, absLowerPath)) { + const old = this.outputFiles[ + this.absPathToOutputIndex[absLowerPath]]; - if (old) { // If the old file is just an empty stub, let the new file take // precedence over it. if (old.implicit === true) { @@ -213,7 +232,7 @@ export default class ImportScanner { } } else { - this.absPathToOutputIndex[absPath] = + this.absPathToOutputIndex[absLowerPath] = this.outputFiles.push(file) - 1; return file; @@ -254,11 +273,82 @@ export default class ImportScanner { // multiple times with the same sourcePath. #6422 this._combineFiles(this._getFile(absPath), file); } + + this._addFileByRealPath(file, this._realPath(absPath)); }); return this; } + _addFileByRealPath(file, realPath) { + assert.ok(isObject(file)); + assert.strictEqual(typeof realPath, "string"); + + if (! has(this.realPathToFiles, realPath)) { + this.realPathToFiles[realPath] = []; + } + + const files = this.realPathToFiles[realPath]; + + if (files.indexOf(file) < 0) { + files.push(file); + } + + return file; + } + + _getInfoByRealPath(realPath) { + assert.strictEqual(typeof realPath, "string"); + const files = this.realPathToFiles[realPath]; + if (files && files.length > 0) { + const firstFile = files[0]; + return { + data: firstFile.data, + dataString: firstFile.dataString, + hash: firstFile.hash, + }; + } + return null; + } + + _realPath(absPath) { + if (has(this.realPathCache, absPath)) { + return this.realPathCache[absPath]; + } + + let relativePath = pathRelative(this.sourceRoot, absPath); + if (relativePath.startsWith("..")) { + // If the absPath is outside this.sourceRoot, assume it's real. + return this.realPathCache[absPath] = absPath; + } + + let foundSymbolicLink = false; + + while (! foundSymbolicLink) { + const testPath = pathJoin(this.sourceRoot, relativePath); + if (testPath === this.sourceRoot) { + // Don't test the sourceRoot itself. + break; + } + + const lstat = optimisticLStatOrNull(testPath); + if (lstat && lstat.isSymbolicLink()) { + foundSymbolicLink = true; + break + } + + relativePath = pathDirname(relativePath); + } + + if (foundSymbolicLink) { + // Call the actual realpathOrNull function only if there were any + // symlinks involved in the relative path within this.sourceRoot. + return this.realPathCache[absPath] = realpathOrNull(absPath); + } + + return this.realPathCache[absPath] = absPath; + } + // Make sure file.sourcePath is defined, and handle the possibility that // file.targetPath differs from file.sourcePath. _checkSourceAndTargetPaths(file) { @@ -538,7 +628,45 @@ export default class ImportScanner { }); } + _mergeFilesWithSameRealPath() { + Object.keys(this.realPathToFiles).forEach(realPath => { + const files = this.realPathToFiles[realPath]; + if (! files || files.length < 2) { + return; + } + + // We have multiple files that share the same realPath, so we need + // to figure out which one should actually contain the data, and + // which one(s) should merely be aliases to the data container. + + let container = files[0]; + + // Take the first file inside node_modules as the container. If none + // found, default to the first file in the list. It's important to + // let node_modules files be the containers if possible, since some + // npm packages rely on having module IDs that appear to be within a + // node_modules directory. + files.some(file => { + if (file.absModuleId && + file.absModuleId.startsWith("/node_modules/")) { + container = file; + return true; + } + }); + + // Alias every non-container file to container.absModuleId. + files.forEach(file => { + if (file !== container) { + file.alias = file.alias || {}; + file.alias.absModuleId = container.absModuleId; + } + }); + }); + } + getOutputFiles() { + this._mergeFilesWithSameRealPath(); + // Return all installable output files that are either eager or // imported (statically or dynamically). return this.outputFiles.filter(file => { @@ -726,11 +854,7 @@ export default class ImportScanner { info.dynamic); const resolved = this._resolve(file, id, dynamic); - if (! resolved) { - return; - } - - const absImportedPath = resolved.path; + const absImportedPath = resolved && resolved.path; if (! absImportedPath) { return; } @@ -761,54 +885,15 @@ export default class ImportScanner { return; } - const absModuleId = this._getAbsModuleId(absImportedPath); - if (! absModuleId) { - // The given path cannot be installed on this architecture. - return; - } - - info.absModuleId = absModuleId; - - // If the module is not readable, _readModule may return - // null. Otherwise it will return an object with .data, .dataString, - // and .hash properties. - depFile = this._readModule(absImportedPath); + depFile = this._readDepFile(absImportedPath); if (! depFile) { return; } - depFile.type = "js"; // TODO Is this correct? - depFile.sourcePath = pathRelative(this.sourceRoot, absImportedPath); - depFile.absModuleId = absModuleId; - depFile.servePath = stripLeadingSlash(absModuleId); - depFile.lazy = true; - // Setting depFile.imported = false is necessary so that - // this._scanFile(depFile, dynamic) doesn't think the file has been - // scanned already and return immediately. - depFile.imported = false; - // Append this file to the output array and record its index. this._addFile(absImportedPath, depFile); - // On the server, modules in node_modules directories will be - // handled natively by Node, so we don't need to build a - // meteorInstall-style bundle beyond the entry-point module. - if (! this.isWeb() && - depFile.absModuleId.startsWith("/node_modules/") && - // If optimistic functions care about this file, e.g. because it - // resides in a linked npm package, then we should allow it to - // be watched by including it in the server bundle by not - // returning here. Note that inclusion in the server bundle is - // an unnecessary consequence of this logic, since Node will - // still evaluate this module natively on the server. What we - // really care about is watching the file for changes. - ! shouldWatch(absImportedPath)) { - // Since we're not going to call this._scanFile(depFile, dynamic) - // below, this is our last chance to update depFile.imported. - depFile.imported = dynamic ? "dynamic" : true; - return; - } - + // Recursively scan the module's imported dependencies. this._scanFile(depFile, dynamic); }); } @@ -889,6 +974,73 @@ export default class ImportScanner { return info; } + _readDepFile(absPath) { + const absModuleId = this._getAbsModuleId(absPath); + if (! absModuleId) { + // The given path cannot be installed on this architecture. + return null; + } + + const realPath = this._realPath(absPath); + + let depFile = this._getInfoByRealPath(realPath); + if (depFile) { + // If we already have a file with the same real path, use its data + // rather than reading the file again, or generating a stub. This + // logic enables selective compilation of node_modules in an elegant + // way: just expose the package directory within the application + // (outside of node_modules) using a symlink, so that it will be + // compiled as application code. When the package is imported from + // node_modules, the compiled version will be used instead of the + // raw version found in node_modules. See also: + // https://github.com/meteor/meteor-feature-requests/issues/6 + + } else if (! this.isWeb() && + absModuleId.startsWith("/node_modules/")) { + // On the server, modules in node_modules directories will be + // handled natively by Node, so we just need to generate a stub + // module that calls module.useNode(), rather than calling + // this._readModule to read the actual module file. Note that + // useNodeStub includes an empty .deps property, which will make + // this._scanFile(depFile, dynamic) return immediately. + depFile = { ...useNodeStub }; + + // If optimistic functions care about this file, e.g. because it + // resides in a linked npm package, then we should allow it to + // be watched even though we are replacing it with a stub that + // merely calls module.useNode(). + if (shouldWatch(absPath)) { + this.watchSet.addFile( + absPath, + optimisticHashOrNull(absPath), + ); + } + + } else { + // If the module is not readable, _readModule may return + // null. Otherwise it will return an object with .data, + // .dataString, and .hash properties. + depFile = this._readModule(absPath); + if (! depFile) { + return null; + } + } + + depFile.type = "js"; // TODO Is this correct? + depFile.sourcePath = pathRelative(this.sourceRoot, absPath); + depFile.absModuleId = absModuleId; + depFile.servePath = stripLeadingSlash(absModuleId); + depFile.lazy = true; + // Setting depFile.imported = false is necessary so that + // this._scanFile(depFile, dynamic) doesn't think the file has been + // scanned already and return immediately. + depFile.imported = false; + + this._addFileByRealPath(depFile, realPath); + + return depFile; + } + // Returns an absolute module identifier indicating where to install the // given file via meteorInstall. May return undefined if the file should // not be installed on the current architecture. @@ -1232,6 +1384,7 @@ each([ "_findImportedModuleIdentifiers", "_getAbsModuleId", "_addPkgJsonToOutput", + "_realPath", "_resolvePkgJsonBrowserAliases", ], funcName => { ImportScanner.prototype[funcName] = Profile( diff --git a/tools/tests/apps/modules/imports/links/acorn/LICENSE b/tools/tests/apps/modules/imports/links/acorn/LICENSE new file mode 100644 index 0000000000..2c0632b6a7 --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2012-2018 by various contributors (see AUTHORS) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/tools/tests/apps/modules/imports/links/acorn/package.json b/tools/tests/apps/modules/imports/links/acorn/package.json new file mode 100644 index 0000000000..1b47c365cc --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/package.json @@ -0,0 +1,51 @@ +{ + "name": "acorn", + "description": "ECMAScript parser", + "homepage": "https://github.com/acornjs/acorn", + "main": "src/index.js", + "version": "5.5.3", + "engines": { + "node": ">=0.4.0" + }, + "maintainers": [ + { + "name": "Marijn Haverbeke", + "email": "marijnh@gmail.com", + "web": "http://marijnhaverbeke.nl" + }, + { + "name": "Ingvar Stepanyan", + "email": "me@rreverser.com", + "web": "http://rreverser.com/" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/acornjs/acorn.git" + }, + "license": "MIT", + "scripts": { + "prepare": "npm test", + "test": "node test/run.js && node test/lint.js", + "pretest": "npm run build:main && npm run build:loose", + "test:test262": "node bin/run_test262.js", + "build": "npm run build:main && npm run build:walk && npm run build:loose && npm run build:bin", + "build:main": "rollup -c rollup/config.main.js", + "build:walk": "rollup -c rollup/config.walk.js", + "build:loose": "rollup -c rollup/config.loose.js && rollup -c rollup/config.loose_es.js", + "lint": "eslint src/" + }, + "devDependencies": { + "eslint": "^4.10.0", + "eslint-config-standard": "^10.2.1", + "eslint-plugin-import": "^2.2.0", + "eslint-plugin-node": "^5.2.1", + "eslint-plugin-promise": "^3.5.0", + "eslint-plugin-standard": "^3.0.1", + "rollup": "^0.45.0", + "rollup-plugin-buble": "^0.16.0", + "test262": "git+https://github.com/tc39/test262.git#18c1e799a01cc976695983b61e225ce7959bdd91", + "test262-parser-runner": "^0.3.1", + "unicode-10.0.0": "^0.7.5" + } +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/.eslintrc b/tools/tests/apps/modules/imports/links/acorn/src/.eslintrc new file mode 100644 index 0000000000..e636ee75db --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/.eslintrc @@ -0,0 +1,33 @@ +{ + "extends": [ + "eslint:recommended", + "standard", + "plugin:import/errors", + "plugin:import/warnings" + ], + "rules": { + "curly": "off", + "eqeqeq": "off", + "indent": ["error", 2, { "SwitchCase": 0, "VariableDeclarator": 2, "CallExpression": { arguments: "off" } }], + "new-parens": "off", + "no-case-declarations": "off", + "no-cond-assign": "off", + "no-fallthrough": "off", + "no-labels": "off", + "no-mixed-operators": "off", + "no-return-assign": "off", + "no-unused-labels": "error", + "no-var": "error", + "object-curly-spacing": ["error", "never"], + "one-var": "off", + "quotes": ["error", "double"], + "semi-spacing": "off", + "space-before-function-paren": ["error", "never"] + }, + "globals": { + "Packages": false + }, + "plugins": [ + "import" + ] +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/bin/.eslintrc b/tools/tests/apps/modules/imports/links/acorn/src/bin/.eslintrc new file mode 100644 index 0000000000..2598b25f5f --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/bin/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": "../.eslintrc", + "rules": { + "no-console": "off" + } +} \ No newline at end of file diff --git a/tools/tests/apps/modules/imports/links/acorn/src/bin/acorn.js b/tools/tests/apps/modules/imports/links/acorn/src/bin/acorn.js new file mode 100644 index 0000000000..d5fe42521a --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/bin/acorn.js @@ -0,0 +1,62 @@ +import {basename} from "path" +import {readFileSync as readFile} from "fs" +import * as acorn from "acorn" + +let infile, forceFile, silent = false, compact = false, tokenize = false +const options = {} + +function help(status) { + const print = (status == 0) ? console.log : console.error + print("usage: " + basename(process.argv[1]) + " [--ecma3|--ecma5|--ecma6|--ecma7|--ecma8|--ecma9|...|--ecma2015|--ecma2016|--ecma2017|--ecma2018|...]") + print(" [--tokenize] [--locations] [---allow-hash-bang] [--compact] [--silent] [--module] [--help] [--] [infile]") + process.exit(status) +} + +for (let i = 2; i < process.argv.length; ++i) { + const arg = process.argv[i] + if ((arg == "-" || arg[0] != "-") && !infile) infile = arg + else if (arg == "--" && !infile && i + 2 == process.argv.length) forceFile = infile = process.argv[++i] + else if (arg == "--locations") options.locations = true + else if (arg == "--allow-hash-bang") options.allowHashBang = true + else if (arg == "--silent") silent = true + else if (arg == "--compact") compact = true + else if (arg == "--help") help(0) + else if (arg == "--tokenize") tokenize = true + else if (arg == "--module") options.sourceType = "module" + else { + let match = arg.match(/^--ecma(\d+)$/) + if (match) + options.ecmaVersion = +match[1] + else + help(1) + } +} + +function run(code) { + let result + try { + if (!tokenize) { + result = acorn.parse(code, options) + } else { + result = [] + let tokenizer = acorn.tokenizer(code, options), token + do { + token = tokenizer.getToken() + result.push(token) + } while (token.type != acorn.tokTypes.eof) + } + } catch (e) { + console.error(e.message) + process.exit(1) + } + if (!silent) console.log(JSON.stringify(result, null, compact ? null : 2)) +} + +if (forceFile || infile && infile != "-") { + run(readFile(infile, "utf8")) +} else { + let code = "" + process.stdin.resume() + process.stdin.on("data", chunk => code += chunk) + process.stdin.on("end", () => run(code)) +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/expression.js b/tools/tests/apps/modules/imports/links/acorn/src/expression.js new file mode 100644 index 0000000000..79cd9b559f --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/expression.js @@ -0,0 +1,909 @@ +// A recursive descent parser operates by defining functions for all +// syntactic elements, and recursively calling those, each function +// advancing the input stream and returning an AST node. Precedence +// of constructs (for example, the fact that `!x[1]` means `!(x[1])` +// instead of `(!x)[1]` is handled by the fact that the parser +// function that parses unary prefix operators is called first, and +// in turn calls the function that parses `[]` subscripts — that +// way, it'll receive the node for `x[1]` already parsed, and wraps +// *that* in the unary operator node. +// +// Acorn uses an [operator precedence parser][opp] to handle binary +// operator precedence, because it is much more compact than using +// the technique outlined above, which uses different, nesting +// functions to specify precedence, for all of the ten binary +// precedence levels that JavaScript defines. +// +// [opp]: http://en.wikipedia.org/wiki/Operator-precedence_parser + +import {types as tt} from "./tokentype" +import {Parser} from "./state" +import {DestructuringErrors} from "./parseutil" +import {lineBreak} from "./whitespace" + +const pp = Parser.prototype + +// Check if property name clashes with already added. +// Object/class getters and setters are not allowed to clash — +// either with each other or with an init property — and in +// strict mode, init properties are also not allowed to be repeated. + +pp.checkPropClash = function(prop, propHash, refDestructuringErrors) { + if (this.options.ecmaVersion >= 9 && prop.type === "SpreadElement") + return + if (this.options.ecmaVersion >= 6 && (prop.computed || prop.method || prop.shorthand)) + return + let {key} = prop, name + switch (key.type) { + case "Identifier": name = key.name; break + case "Literal": name = String(key.value); break + default: return + } + let {kind} = prop + if (this.options.ecmaVersion >= 6) { + if (name === "__proto__" && kind === "init") { + if (propHash.proto) { + if (refDestructuringErrors && refDestructuringErrors.doubleProto < 0) refDestructuringErrors.doubleProto = key.start + // Backwards-compat kludge. Can be removed in version 6.0 + else this.raiseRecoverable(key.start, "Redefinition of __proto__ property") + } + propHash.proto = true + } + return + } + name = "$" + name + let other = propHash[name] + if (other) { + let redefinition + if (kind === "init") { + redefinition = this.strict && other.init || other.get || other.set + } else { + redefinition = other.init || other[kind] + } + if (redefinition) + this.raiseRecoverable(key.start, "Redefinition of property") + } else { + other = propHash[name] = { + init: false, + get: false, + set: false + } + } + other[kind] = true +} + +// ### Expression parsing + +// These nest, from the most general expression type at the top to +// 'atomic', nondivisible expression types at the bottom. Most of +// the functions will simply let the function(s) below them parse, +// and, *if* the syntactic construct they handle is present, wrap +// the AST node that the inner parser gave them in another node. + +// Parse a full expression. The optional arguments are used to +// forbid the `in` operator (in for loops initalization expressions) +// and provide reference for storing '=' operator inside shorthand +// property assignment in contexts where both object expression +// and object pattern might appear (so it's possible to raise +// delayed syntax error at correct position). + +pp.parseExpression = function(noIn, refDestructuringErrors) { + let startPos = this.start, startLoc = this.startLoc + let expr = this.parseMaybeAssign(noIn, refDestructuringErrors) + if (this.type === tt.comma) { + let node = this.startNodeAt(startPos, startLoc) + node.expressions = [expr] + while (this.eat(tt.comma)) node.expressions.push(this.parseMaybeAssign(noIn, refDestructuringErrors)) + return this.finishNode(node, "SequenceExpression") + } + return expr +} + +// Parse an assignment expression. This includes applications of +// operators like `+=`. + +pp.parseMaybeAssign = function(noIn, refDestructuringErrors, afterLeftParse) { + if (this.inGenerator && this.isContextual("yield")) return this.parseYield() + + let ownDestructuringErrors = false, oldParenAssign = -1, oldTrailingComma = -1 + if (refDestructuringErrors) { + oldParenAssign = refDestructuringErrors.parenthesizedAssign + oldTrailingComma = refDestructuringErrors.trailingComma + refDestructuringErrors.parenthesizedAssign = refDestructuringErrors.trailingComma = -1 + } else { + refDestructuringErrors = new DestructuringErrors + ownDestructuringErrors = true + } + + let startPos = this.start, startLoc = this.startLoc + if (this.type == tt.parenL || this.type == tt.name) + this.potentialArrowAt = this.start + let left = this.parseMaybeConditional(noIn, refDestructuringErrors) + if (afterLeftParse) left = afterLeftParse.call(this, left, startPos, startLoc) + if (this.type.isAssign) { + let node = this.startNodeAt(startPos, startLoc) + node.operator = this.value + node.left = this.type === tt.eq ? this.toAssignable(left, false, refDestructuringErrors) : left + if (!ownDestructuringErrors) DestructuringErrors.call(refDestructuringErrors) + refDestructuringErrors.shorthandAssign = -1 // reset because shorthand default was used correctly + this.checkLVal(left) + this.next() + node.right = this.parseMaybeAssign(noIn) + return this.finishNode(node, "AssignmentExpression") + } else { + if (ownDestructuringErrors) this.checkExpressionErrors(refDestructuringErrors, true) + } + if (oldParenAssign > -1) refDestructuringErrors.parenthesizedAssign = oldParenAssign + if (oldTrailingComma > -1) refDestructuringErrors.trailingComma = oldTrailingComma + return left +} + +// Parse a ternary conditional (`?:`) operator. + +pp.parseMaybeConditional = function(noIn, refDestructuringErrors) { + let startPos = this.start, startLoc = this.startLoc + let expr = this.parseExprOps(noIn, refDestructuringErrors) + if (this.checkExpressionErrors(refDestructuringErrors)) return expr + if (this.eat(tt.question)) { + let node = this.startNodeAt(startPos, startLoc) + node.test = expr + node.consequent = this.parseMaybeAssign() + this.expect(tt.colon) + node.alternate = this.parseMaybeAssign(noIn) + return this.finishNode(node, "ConditionalExpression") + } + return expr +} + +// Start the precedence parser. + +pp.parseExprOps = function(noIn, refDestructuringErrors) { + let startPos = this.start, startLoc = this.startLoc + let expr = this.parseMaybeUnary(refDestructuringErrors, false) + if (this.checkExpressionErrors(refDestructuringErrors)) return expr + return expr.start == startPos && expr.type === "ArrowFunctionExpression" ? expr : this.parseExprOp(expr, startPos, startLoc, -1, noIn) +} + +// Parse binary operators with the operator precedence parsing +// algorithm. `left` is the left-hand side of the operator. +// `minPrec` provides context that allows the function to stop and +// defer further parser to one of its callers when it encounters an +// operator that has a lower precedence than the set it is parsing. + +pp.parseExprOp = function(left, leftStartPos, leftStartLoc, minPrec, noIn) { + let prec = this.type.binop + if (prec != null && (!noIn || this.type !== tt._in)) { + if (prec > minPrec) { + let logical = this.type === tt.logicalOR || this.type === tt.logicalAND + let op = this.value + this.next() + let startPos = this.start, startLoc = this.startLoc + let right = this.parseExprOp(this.parseMaybeUnary(null, false), startPos, startLoc, prec, noIn) + let node = this.buildBinary(leftStartPos, leftStartLoc, left, right, op, logical) + return this.parseExprOp(node, leftStartPos, leftStartLoc, minPrec, noIn) + } + } + return left +} + +pp.buildBinary = function(startPos, startLoc, left, right, op, logical) { + let node = this.startNodeAt(startPos, startLoc) + node.left = left + node.operator = op + node.right = right + return this.finishNode(node, logical ? "LogicalExpression" : "BinaryExpression") +} + +// Parse unary operators, both prefix and postfix. + +pp.parseMaybeUnary = function(refDestructuringErrors, sawUnary) { + let startPos = this.start, startLoc = this.startLoc, expr + if (this.inAsync && this.isContextual("await")) { + expr = this.parseAwait() + sawUnary = true + } else if (this.type.prefix) { + let node = this.startNode(), update = this.type === tt.incDec + node.operator = this.value + node.prefix = true + this.next() + node.argument = this.parseMaybeUnary(null, true) + this.checkExpressionErrors(refDestructuringErrors, true) + if (update) this.checkLVal(node.argument) + else if (this.strict && node.operator === "delete" && + node.argument.type === "Identifier") + this.raiseRecoverable(node.start, "Deleting local variable in strict mode") + else sawUnary = true + expr = this.finishNode(node, update ? "UpdateExpression" : "UnaryExpression") + } else { + expr = this.parseExprSubscripts(refDestructuringErrors) + if (this.checkExpressionErrors(refDestructuringErrors)) return expr + while (this.type.postfix && !this.canInsertSemicolon()) { + let node = this.startNodeAt(startPos, startLoc) + node.operator = this.value + node.prefix = false + node.argument = expr + this.checkLVal(expr) + this.next() + expr = this.finishNode(node, "UpdateExpression") + } + } + + if (!sawUnary && this.eat(tt.starstar)) + return this.buildBinary(startPos, startLoc, expr, this.parseMaybeUnary(null, false), "**", false) + else + return expr +} + +// Parse call, dot, and `[]`-subscript expressions. + +pp.parseExprSubscripts = function(refDestructuringErrors) { + let startPos = this.start, startLoc = this.startLoc + let expr = this.parseExprAtom(refDestructuringErrors) + let skipArrowSubscripts = expr.type === "ArrowFunctionExpression" && this.input.slice(this.lastTokStart, this.lastTokEnd) !== ")" + if (this.checkExpressionErrors(refDestructuringErrors) || skipArrowSubscripts) return expr + let result = this.parseSubscripts(expr, startPos, startLoc) + if (refDestructuringErrors && result.type === "MemberExpression") { + if (refDestructuringErrors.parenthesizedAssign >= result.start) refDestructuringErrors.parenthesizedAssign = -1 + if (refDestructuringErrors.parenthesizedBind >= result.start) refDestructuringErrors.parenthesizedBind = -1 + } + return result +} + +pp.parseSubscripts = function(base, startPos, startLoc, noCalls) { + let maybeAsyncArrow = this.options.ecmaVersion >= 8 && base.type === "Identifier" && base.name === "async" && + this.lastTokEnd == base.end && !this.canInsertSemicolon() && this.input.slice(base.start, base.end) === "async" + for (let computed;;) { + if ((computed = this.eat(tt.bracketL)) || this.eat(tt.dot)) { + let node = this.startNodeAt(startPos, startLoc) + node.object = base + node.property = computed ? this.parseExpression() : this.parseIdent(true) + node.computed = !!computed + if (computed) this.expect(tt.bracketR) + base = this.finishNode(node, "MemberExpression") + } else if (!noCalls && this.eat(tt.parenL)) { + let refDestructuringErrors = new DestructuringErrors, oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos + this.yieldPos = 0 + this.awaitPos = 0 + let exprList = this.parseExprList(tt.parenR, this.options.ecmaVersion >= 8, false, refDestructuringErrors) + if (maybeAsyncArrow && !this.canInsertSemicolon() && this.eat(tt.arrow)) { + this.checkPatternErrors(refDestructuringErrors, false) + this.checkYieldAwaitInDefaultParams() + this.yieldPos = oldYieldPos + this.awaitPos = oldAwaitPos + return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), exprList, true) + } + this.checkExpressionErrors(refDestructuringErrors, true) + this.yieldPos = oldYieldPos || this.yieldPos + this.awaitPos = oldAwaitPos || this.awaitPos + let node = this.startNodeAt(startPos, startLoc) + node.callee = base + node.arguments = exprList + base = this.finishNode(node, "CallExpression") + } else if (this.type === tt.backQuote) { + let node = this.startNodeAt(startPos, startLoc) + node.tag = base + node.quasi = this.parseTemplate({isTagged: true}) + base = this.finishNode(node, "TaggedTemplateExpression") + } else { + return base + } + } +} + +// Parse an atomic expression — either a single token that is an +// expression, an expression started by a keyword like `function` or +// `new`, or an expression wrapped in punctuation like `()`, `[]`, +// or `{}`. + +pp.parseExprAtom = function(refDestructuringErrors) { + let node, canBeArrow = this.potentialArrowAt == this.start + switch (this.type) { + case tt._super: + if (!this.inFunction) + this.raise(this.start, "'super' outside of function or class") + node = this.startNode() + this.next() + // The `super` keyword can appear at below: + // SuperProperty: + // super [ Expression ] + // super . IdentifierName + // SuperCall: + // super Arguments + if (this.type !== tt.dot && this.type !== tt.bracketL && this.type !== tt.parenL) + this.unexpected() + return this.finishNode(node, "Super") + + case tt._this: + node = this.startNode() + this.next() + return this.finishNode(node, "ThisExpression") + + case tt.name: + let startPos = this.start, startLoc = this.startLoc, containsEsc = this.containsEsc + let id = this.parseIdent(this.type !== tt.name) + if (this.options.ecmaVersion >= 8 && !containsEsc && id.name === "async" && !this.canInsertSemicolon() && this.eat(tt._function)) + return this.parseFunction(this.startNodeAt(startPos, startLoc), false, false, true) + if (canBeArrow && !this.canInsertSemicolon()) { + if (this.eat(tt.arrow)) + return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), [id], false) + if (this.options.ecmaVersion >= 8 && id.name === "async" && this.type === tt.name && !containsEsc) { + id = this.parseIdent() + if (this.canInsertSemicolon() || !this.eat(tt.arrow)) + this.unexpected() + return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), [id], true) + } + } + return id + + case tt.regexp: + let value = this.value + node = this.parseLiteral(value.value) + node.regex = {pattern: value.pattern, flags: value.flags} + return node + + case tt.num: case tt.string: + return this.parseLiteral(this.value) + + case tt._null: case tt._true: case tt._false: + node = this.startNode() + node.value = this.type === tt._null ? null : this.type === tt._true + node.raw = this.type.keyword + this.next() + return this.finishNode(node, "Literal") + + case tt.parenL: + let start = this.start, expr = this.parseParenAndDistinguishExpression(canBeArrow) + if (refDestructuringErrors) { + if (refDestructuringErrors.parenthesizedAssign < 0 && !this.isSimpleAssignTarget(expr)) + refDestructuringErrors.parenthesizedAssign = start + if (refDestructuringErrors.parenthesizedBind < 0) + refDestructuringErrors.parenthesizedBind = start + } + return expr + + case tt.bracketL: + node = this.startNode() + this.next() + node.elements = this.parseExprList(tt.bracketR, true, true, refDestructuringErrors) + return this.finishNode(node, "ArrayExpression") + + case tt.braceL: + return this.parseObj(false, refDestructuringErrors) + + case tt._function: + node = this.startNode() + this.next() + return this.parseFunction(node, false) + + case tt._class: + return this.parseClass(this.startNode(), false) + + case tt._new: + return this.parseNew() + + case tt.backQuote: + return this.parseTemplate() + + default: + this.unexpected() + } +} + +pp.parseLiteral = function(value) { + let node = this.startNode() + node.value = value + node.raw = this.input.slice(this.start, this.end) + this.next() + return this.finishNode(node, "Literal") +} + +pp.parseParenExpression = function() { + this.expect(tt.parenL) + let val = this.parseExpression() + this.expect(tt.parenR) + return val +} + +pp.parseParenAndDistinguishExpression = function(canBeArrow) { + let startPos = this.start, startLoc = this.startLoc, val, allowTrailingComma = this.options.ecmaVersion >= 8 + if (this.options.ecmaVersion >= 6) { + this.next() + + let innerStartPos = this.start, innerStartLoc = this.startLoc + let exprList = [], first = true, lastIsComma = false + let refDestructuringErrors = new DestructuringErrors, oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, spreadStart + this.yieldPos = 0 + this.awaitPos = 0 + while (this.type !== tt.parenR) { + first ? first = false : this.expect(tt.comma) + if (allowTrailingComma && this.afterTrailingComma(tt.parenR, true)) { + lastIsComma = true + break + } else if (this.type === tt.ellipsis) { + spreadStart = this.start + exprList.push(this.parseParenItem(this.parseRestBinding())) + if (this.type === tt.comma) this.raise(this.start, "Comma is not permitted after the rest element") + break + } else { + exprList.push(this.parseMaybeAssign(false, refDestructuringErrors, this.parseParenItem)) + } + } + let innerEndPos = this.start, innerEndLoc = this.startLoc + this.expect(tt.parenR) + + if (canBeArrow && !this.canInsertSemicolon() && this.eat(tt.arrow)) { + this.checkPatternErrors(refDestructuringErrors, false) + this.checkYieldAwaitInDefaultParams() + this.yieldPos = oldYieldPos + this.awaitPos = oldAwaitPos + return this.parseParenArrowList(startPos, startLoc, exprList) + } + + if (!exprList.length || lastIsComma) this.unexpected(this.lastTokStart) + if (spreadStart) this.unexpected(spreadStart) + this.checkExpressionErrors(refDestructuringErrors, true) + this.yieldPos = oldYieldPos || this.yieldPos + this.awaitPos = oldAwaitPos || this.awaitPos + + if (exprList.length > 1) { + val = this.startNodeAt(innerStartPos, innerStartLoc) + val.expressions = exprList + this.finishNodeAt(val, "SequenceExpression", innerEndPos, innerEndLoc) + } else { + val = exprList[0] + } + } else { + val = this.parseParenExpression() + } + + if (this.options.preserveParens) { + let par = this.startNodeAt(startPos, startLoc) + par.expression = val + return this.finishNode(par, "ParenthesizedExpression") + } else { + return val + } +} + +pp.parseParenItem = function(item) { + return item +} + +pp.parseParenArrowList = function(startPos, startLoc, exprList) { + return this.parseArrowExpression(this.startNodeAt(startPos, startLoc), exprList) +} + +// New's precedence is slightly tricky. It must allow its argument to +// be a `[]` or dot subscript expression, but not a call — at least, +// not without wrapping it in parentheses. Thus, it uses the noCalls +// argument to parseSubscripts to prevent it from consuming the +// argument list. + +const empty = [] + +pp.parseNew = function() { + let node = this.startNode() + let meta = this.parseIdent(true) + if (this.options.ecmaVersion >= 6 && this.eat(tt.dot)) { + node.meta = meta + let containsEsc = this.containsEsc + node.property = this.parseIdent(true) + if (node.property.name !== "target" || containsEsc) + this.raiseRecoverable(node.property.start, "The only valid meta property for new is new.target") + if (!this.inFunction) + this.raiseRecoverable(node.start, "new.target can only be used in functions") + return this.finishNode(node, "MetaProperty") + } + let startPos = this.start, startLoc = this.startLoc + node.callee = this.parseSubscripts(this.parseExprAtom(), startPos, startLoc, true) + if (this.eat(tt.parenL)) node.arguments = this.parseExprList(tt.parenR, this.options.ecmaVersion >= 8, false) + else node.arguments = empty + return this.finishNode(node, "NewExpression") +} + +// Parse template expression. + +pp.parseTemplateElement = function({isTagged}) { + let elem = this.startNode() + if (this.type === tt.invalidTemplate) { + if (!isTagged) { + this.raiseRecoverable(this.start, "Bad escape sequence in untagged template literal") + } + elem.value = { + raw: this.value, + cooked: null + } + } else { + elem.value = { + raw: this.input.slice(this.start, this.end).replace(/\r\n?/g, "\n"), + cooked: this.value + } + } + this.next() + elem.tail = this.type === tt.backQuote + return this.finishNode(elem, "TemplateElement") +} + +pp.parseTemplate = function({isTagged = false} = {}) { + let node = this.startNode() + this.next() + node.expressions = [] + let curElt = this.parseTemplateElement({isTagged}) + node.quasis = [curElt] + while (!curElt.tail) { + this.expect(tt.dollarBraceL) + node.expressions.push(this.parseExpression()) + this.expect(tt.braceR) + node.quasis.push(curElt = this.parseTemplateElement({isTagged})) + } + this.next() + return this.finishNode(node, "TemplateLiteral") +} + +pp.isAsyncProp = function(prop) { + return !prop.computed && prop.key.type === "Identifier" && prop.key.name === "async" && + (this.type === tt.name || this.type === tt.num || this.type === tt.string || this.type === tt.bracketL || this.type.keyword || (this.options.ecmaVersion >= 9 && this.type === tt.star)) && + !lineBreak.test(this.input.slice(this.lastTokEnd, this.start)) +} + +// Parse an object literal or binding pattern. + +pp.parseObj = function(isPattern, refDestructuringErrors) { + let node = this.startNode(), first = true, propHash = {} + node.properties = [] + this.next() + while (!this.eat(tt.braceR)) { + if (!first) { + this.expect(tt.comma) + if (this.afterTrailingComma(tt.braceR)) break + } else first = false + + const prop = this.parseProperty(isPattern, refDestructuringErrors) + if (!isPattern) this.checkPropClash(prop, propHash, refDestructuringErrors) + node.properties.push(prop) + } + return this.finishNode(node, isPattern ? "ObjectPattern" : "ObjectExpression") +} + +pp.parseProperty = function(isPattern, refDestructuringErrors) { + let prop = this.startNode(), isGenerator, isAsync, startPos, startLoc + if (this.options.ecmaVersion >= 9 && this.eat(tt.ellipsis)) { + if (isPattern) { + prop.argument = this.parseIdent(false) + if (this.type === tt.comma) { + this.raise(this.start, "Comma is not permitted after the rest element") + } + return this.finishNode(prop, "RestElement") + } + // To disallow parenthesized identifier via `this.toAssignable()`. + if (this.type === tt.parenL && refDestructuringErrors) { + if (refDestructuringErrors.parenthesizedAssign < 0) { + refDestructuringErrors.parenthesizedAssign = this.start + } + if (refDestructuringErrors.parenthesizedBind < 0) { + refDestructuringErrors.parenthesizedBind = this.start + } + } + // Parse argument. + prop.argument = this.parseMaybeAssign(false, refDestructuringErrors) + // To disallow trailing comma via `this.toAssignable()`. + if (this.type === tt.comma && refDestructuringErrors && refDestructuringErrors.trailingComma < 0) { + refDestructuringErrors.trailingComma = this.start + } + // Finish + return this.finishNode(prop, "SpreadElement") + } + if (this.options.ecmaVersion >= 6) { + prop.method = false + prop.shorthand = false + if (isPattern || refDestructuringErrors) { + startPos = this.start + startLoc = this.startLoc + } + if (!isPattern) + isGenerator = this.eat(tt.star) + } + let containsEsc = this.containsEsc + this.parsePropertyName(prop) + if (!isPattern && !containsEsc && this.options.ecmaVersion >= 8 && !isGenerator && this.isAsyncProp(prop)) { + isAsync = true + isGenerator = this.options.ecmaVersion >= 9 && this.eat(tt.star) + this.parsePropertyName(prop, refDestructuringErrors) + } else { + isAsync = false + } + this.parsePropertyValue(prop, isPattern, isGenerator, isAsync, startPos, startLoc, refDestructuringErrors, containsEsc) + return this.finishNode(prop, "Property") +} + +pp.parsePropertyValue = function(prop, isPattern, isGenerator, isAsync, startPos, startLoc, refDestructuringErrors, containsEsc) { + if ((isGenerator || isAsync) && this.type === tt.colon) + this.unexpected() + + if (this.eat(tt.colon)) { + prop.value = isPattern ? this.parseMaybeDefault(this.start, this.startLoc) : this.parseMaybeAssign(false, refDestructuringErrors) + prop.kind = "init" + } else if (this.options.ecmaVersion >= 6 && this.type === tt.parenL) { + if (isPattern) this.unexpected() + prop.kind = "init" + prop.method = true + prop.value = this.parseMethod(isGenerator, isAsync) + } else if (!isPattern && !containsEsc && + this.options.ecmaVersion >= 5 && !prop.computed && prop.key.type === "Identifier" && + (prop.key.name === "get" || prop.key.name === "set") && + (this.type != tt.comma && this.type != tt.braceR)) { + if (isGenerator || isAsync) this.unexpected() + prop.kind = prop.key.name + this.parsePropertyName(prop) + prop.value = this.parseMethod(false) + let paramCount = prop.kind === "get" ? 0 : 1 + if (prop.value.params.length !== paramCount) { + let start = prop.value.start + if (prop.kind === "get") + this.raiseRecoverable(start, "getter should have no params") + else + this.raiseRecoverable(start, "setter should have exactly one param") + } else { + if (prop.kind === "set" && prop.value.params[0].type === "RestElement") + this.raiseRecoverable(prop.value.params[0].start, "Setter cannot use rest params") + } + } else if (this.options.ecmaVersion >= 6 && !prop.computed && prop.key.type === "Identifier") { + this.checkUnreserved(prop.key) + prop.kind = "init" + if (isPattern) { + prop.value = this.parseMaybeDefault(startPos, startLoc, prop.key) + } else if (this.type === tt.eq && refDestructuringErrors) { + if (refDestructuringErrors.shorthandAssign < 0) + refDestructuringErrors.shorthandAssign = this.start + prop.value = this.parseMaybeDefault(startPos, startLoc, prop.key) + } else { + prop.value = prop.key + } + prop.shorthand = true + } else this.unexpected() +} + +pp.parsePropertyName = function(prop) { + if (this.options.ecmaVersion >= 6) { + if (this.eat(tt.bracketL)) { + prop.computed = true + prop.key = this.parseMaybeAssign() + this.expect(tt.bracketR) + return prop.key + } else { + prop.computed = false + } + } + return prop.key = this.type === tt.num || this.type === tt.string ? this.parseExprAtom() : this.parseIdent(true) +} + +// Initialize empty function node. + +pp.initFunction = function(node) { + node.id = null + if (this.options.ecmaVersion >= 6) { + node.generator = false + node.expression = false + } + if (this.options.ecmaVersion >= 8) + node.async = false +} + +// Parse object or class method. + +pp.parseMethod = function(isGenerator, isAsync) { + let node = this.startNode(), oldInGen = this.inGenerator, oldInAsync = this.inAsync, + oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldInFunc = this.inFunction + + this.initFunction(node) + if (this.options.ecmaVersion >= 6) + node.generator = isGenerator + if (this.options.ecmaVersion >= 8) + node.async = !!isAsync + + this.inGenerator = node.generator + this.inAsync = node.async + this.yieldPos = 0 + this.awaitPos = 0 + this.inFunction = true + this.enterFunctionScope() + + this.expect(tt.parenL) + node.params = this.parseBindingList(tt.parenR, false, this.options.ecmaVersion >= 8) + this.checkYieldAwaitInDefaultParams() + this.parseFunctionBody(node, false) + + this.inGenerator = oldInGen + this.inAsync = oldInAsync + this.yieldPos = oldYieldPos + this.awaitPos = oldAwaitPos + this.inFunction = oldInFunc + return this.finishNode(node, "FunctionExpression") +} + +// Parse arrow function expression with given parameters. + +pp.parseArrowExpression = function(node, params, isAsync) { + let oldInGen = this.inGenerator, oldInAsync = this.inAsync, + oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldInFunc = this.inFunction + + this.enterFunctionScope() + this.initFunction(node) + if (this.options.ecmaVersion >= 8) + node.async = !!isAsync + + this.inGenerator = false + this.inAsync = node.async + this.yieldPos = 0 + this.awaitPos = 0 + this.inFunction = true + + node.params = this.toAssignableList(params, true) + this.parseFunctionBody(node, true) + + this.inGenerator = oldInGen + this.inAsync = oldInAsync + this.yieldPos = oldYieldPos + this.awaitPos = oldAwaitPos + this.inFunction = oldInFunc + return this.finishNode(node, "ArrowFunctionExpression") +} + +// Parse function body and check parameters. + +pp.parseFunctionBody = function(node, isArrowFunction) { + let isExpression = isArrowFunction && this.type !== tt.braceL + let oldStrict = this.strict, useStrict = false + + if (isExpression) { + node.body = this.parseMaybeAssign() + node.expression = true + this.checkParams(node, false) + } else { + let nonSimple = this.options.ecmaVersion >= 7 && !this.isSimpleParamList(node.params) + if (!oldStrict || nonSimple) { + useStrict = this.strictDirective(this.end) + // If this is a strict mode function, verify that argument names + // are not repeated, and it does not try to bind the words `eval` + // or `arguments`. + if (useStrict && nonSimple) + this.raiseRecoverable(node.start, "Illegal 'use strict' directive in function with non-simple parameter list") + } + // Start a new scope with regard to labels and the `inFunction` + // flag (restore them to their old value afterwards). + let oldLabels = this.labels + this.labels = [] + if (useStrict) this.strict = true + + // Add the params to varDeclaredNames to ensure that an error is thrown + // if a let/const declaration in the function clashes with one of the params. + this.checkParams(node, !oldStrict && !useStrict && !isArrowFunction && this.isSimpleParamList(node.params)) + node.body = this.parseBlock(false) + node.expression = false + this.adaptDirectivePrologue(node.body.body) + this.labels = oldLabels + } + this.exitFunctionScope() + + if (this.strict && node.id) { + // Ensure the function name isn't a forbidden identifier in strict mode, e.g. 'eval' + this.checkLVal(node.id, "none") + } + this.strict = oldStrict +} + +pp.isSimpleParamList = function(params) { + for (let param of params) + if (param.type !== "Identifier") return false + return true +} + +// Checks function params for various disallowed patterns such as using "eval" +// or "arguments" and duplicate parameters. + +pp.checkParams = function(node, allowDuplicates) { + let nameHash = {} + for (let param of node.params) + this.checkLVal(param, "var", allowDuplicates ? null : nameHash) +} + +// Parses a comma-separated list of expressions, and returns them as +// an array. `close` is the token type that ends the list, and +// `allowEmpty` can be turned on to allow subsequent commas with +// nothing in between them to be parsed as `null` (which is needed +// for array literals). + +pp.parseExprList = function(close, allowTrailingComma, allowEmpty, refDestructuringErrors) { + let elts = [], first = true + while (!this.eat(close)) { + if (!first) { + this.expect(tt.comma) + if (allowTrailingComma && this.afterTrailingComma(close)) break + } else first = false + + let elt + if (allowEmpty && this.type === tt.comma) + elt = null + else if (this.type === tt.ellipsis) { + elt = this.parseSpread(refDestructuringErrors) + if (refDestructuringErrors && this.type === tt.comma && refDestructuringErrors.trailingComma < 0) + refDestructuringErrors.trailingComma = this.start + } else { + elt = this.parseMaybeAssign(false, refDestructuringErrors) + } + elts.push(elt) + } + return elts +} + +pp.checkUnreserved = function({start, end, name}) { + if (this.inGenerator && name === "yield") + this.raiseRecoverable(start, "Can not use 'yield' as identifier inside a generator") + if (this.inAsync && name === "await") + this.raiseRecoverable(start, "Can not use 'await' as identifier inside an async function") + if (this.isKeyword(name)) + this.raise(start, `Unexpected keyword '${name}'`) + if (this.options.ecmaVersion < 6 && + this.input.slice(start, end).indexOf("\\") != -1) return + const re = this.strict ? this.reservedWordsStrict : this.reservedWords + if (re.test(name)) { + if (!this.inAsync && name === "await") + this.raiseRecoverable(start, "Can not use keyword 'await' outside an async function") + this.raiseRecoverable(start, `The keyword '${name}' is reserved`) + } +} + +// Parse the next token as an identifier. If `liberal` is true (used +// when parsing properties), it will also convert keywords into +// identifiers. + +pp.parseIdent = function(liberal, isBinding) { + let node = this.startNode() + if (liberal && this.options.allowReserved == "never") liberal = false + if (this.type === tt.name) { + node.name = this.value + } else if (this.type.keyword) { + node.name = this.type.keyword + + // To fix https://github.com/acornjs/acorn/issues/575 + // `class` and `function` keywords push new context into this.context. + // But there is no chance to pop the context if the keyword is consumed as an identifier such as a property name. + // If the previous token is a dot, this does not apply because the context-managing code already ignored the keyword + if ((node.name === "class" || node.name === "function") && + (this.lastTokEnd !== this.lastTokStart + 1 || this.input.charCodeAt(this.lastTokStart) !== 46)) { + this.context.pop() + } + } else { + this.unexpected() + } + this.next() + this.finishNode(node, "Identifier") + if (!liberal) this.checkUnreserved(node) + return node +} + +// Parses yield expression inside generator. + +pp.parseYield = function() { + if (!this.yieldPos) this.yieldPos = this.start + + let node = this.startNode() + this.next() + if (this.type == tt.semi || this.canInsertSemicolon() || (this.type != tt.star && !this.type.startsExpr)) { + node.delegate = false + node.argument = null + } else { + node.delegate = this.eat(tt.star) + node.argument = this.parseMaybeAssign() + } + return this.finishNode(node, "YieldExpression") +} + +pp.parseAwait = function() { + if (!this.awaitPos) this.awaitPos = this.start + + let node = this.startNode() + this.next() + node.argument = this.parseMaybeUnary(null, true) + return this.finishNode(node, "AwaitExpression") +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/identifier.js b/tools/tests/apps/modules/imports/links/acorn/src/identifier.js new file mode 100644 index 0000000000..5b6553b604 --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/identifier.js @@ -0,0 +1,87 @@ +// Reserved word lists for various dialects of the language + +export const reservedWords = { + 3: "abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized throws transient volatile", + 5: "class enum extends super const export import", + 6: "enum", + strict: "implements interface let package private protected public static yield", + strictBind: "eval arguments" +} + +// And the keywords + +const ecma5AndLessKeywords = "break case catch continue debugger default do else finally for function if return switch throw try var while with null true false instanceof typeof void delete new in this" + +export const keywords = { + 5: ecma5AndLessKeywords, + 6: ecma5AndLessKeywords + " const class extends export import super" +} + +export const keywordRelationalOperator = /^in(stanceof)?$/ + +// ## Character categories + +// Big ugly regular expressions that match characters in the +// whitespace, identifier, and identifier-start categories. These +// are only applied when a character is found to actually have a +// code point above 128. +// Generated by `bin/generate-identifier-regex.js`. + +let nonASCIIidentifierStartChars = "\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u037f\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u052f\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u0860-\u086a\u08a0-\u08b4\u08b6-\u08bd\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u09fc\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0af9\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c39\u0c3d\u0c58-\u0c5a\u0c60\u0c61\u0c80\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d54-\u0d56\u0d5f-\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f5\u13f8-\u13fd\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f8\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1877\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191e\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1c80-\u1c88\u1ce9-\u1cec\u1cee-\u1cf1\u1cf5\u1cf6\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2118-\u211d\u2124\u2126\u2128\u212a-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309b-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312e\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fea\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua69d\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua7ae\ua7b0-\ua7b7\ua7f7-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua8fd\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\ua9e0-\ua9e4\ua9e6-\ua9ef\ua9fa-\ua9fe\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa7e-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uab30-\uab5a\uab5c-\uab65\uab70-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc" +let nonASCIIidentifierChars = "\u200c\u200d\xb7\u0300-\u036f\u0387\u0483-\u0487\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u0669\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7\u06e8\u06ea-\u06ed\u06f0-\u06f9\u0711\u0730-\u074a\u07a6-\u07b0\u07c0-\u07c9\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u08d4-\u08e1\u08e3-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962\u0963\u0966-\u096f\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09cb-\u09cd\u09d7\u09e2\u09e3\u09e6-\u09ef\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a66-\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2\u0ae3\u0ae6-\u0aef\u0afa-\u0aff\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b66-\u0b6f\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0be6-\u0bef\u0c00-\u0c03\u0c3e-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0c66-\u0c6f\u0c81-\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0ce6-\u0cef\u0d00-\u0d03\u0d3b\u0d3c\u0d3e-\u0d44\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0d62\u0d63\u0d66-\u0d6f\u0d82\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0de6-\u0def\u0df2\u0df3\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0e50-\u0e59\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0ed0-\u0ed9\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f3e\u0f3f\u0f71-\u0f84\u0f86\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102b-\u103e\u1040-\u1049\u1056-\u1059\u105e-\u1060\u1062-\u1064\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f-\u109d\u135d-\u135f\u1369-\u1371\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b4-\u17d3\u17dd\u17e0-\u17e9\u180b-\u180d\u1810-\u1819\u18a9\u1920-\u192b\u1930-\u193b\u1946-\u194f\u19d0-\u19da\u1a17-\u1a1b\u1a55-\u1a5e\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1ab0-\u1abd\u1b00-\u1b04\u1b34-\u1b44\u1b50-\u1b59\u1b6b-\u1b73\u1b80-\u1b82\u1ba1-\u1bad\u1bb0-\u1bb9\u1be6-\u1bf3\u1c24-\u1c37\u1c40-\u1c49\u1c50-\u1c59\u1cd0-\u1cd2\u1cd4-\u1ce8\u1ced\u1cf2-\u1cf4\u1cf7-\u1cf9\u1dc0-\u1df9\u1dfb-\u1dff\u203f\u2040\u2054\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua620-\ua629\ua66f\ua674-\ua67d\ua69e\ua69f\ua6f0\ua6f1\ua802\ua806\ua80b\ua823-\ua827\ua880\ua881\ua8b4-\ua8c5\ua8d0-\ua8d9\ua8e0-\ua8f1\ua900-\ua909\ua926-\ua92d\ua947-\ua953\ua980-\ua983\ua9b3-\ua9c0\ua9d0-\ua9d9\ua9e5\ua9f0-\ua9f9\uaa29-\uaa36\uaa43\uaa4c\uaa4d\uaa50-\uaa59\uaa7b-\uaa7d\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uaaeb-\uaaef\uaaf5\uaaf6\uabe3-\uabea\uabec\uabed\uabf0-\uabf9\ufb1e\ufe00-\ufe0f\ufe20-\ufe2f\ufe33\ufe34\ufe4d-\ufe4f\uff10-\uff19\uff3f" + +const nonASCIIidentifierStart = new RegExp("[" + nonASCIIidentifierStartChars + "]") +const nonASCIIidentifier = new RegExp("[" + nonASCIIidentifierStartChars + nonASCIIidentifierChars + "]") + +nonASCIIidentifierStartChars = nonASCIIidentifierChars = null + +// These are a run-length and offset encoded representation of the +// >0xffff code points that are a valid part of identifiers. The +// offset starts at 0x10000, and each pair of numbers represents an +// offset to the next range, and then a size of the range. They were +// generated by bin/generate-identifier-regex.js + +// eslint-disable-next-line comma-spacing +const astralIdentifierStartCodes = [0,11,2,25,2,18,2,1,2,14,3,13,35,122,70,52,268,28,4,48,48,31,14,29,6,37,11,29,3,35,5,7,2,4,43,157,19,35,5,35,5,39,9,51,157,310,10,21,11,7,153,5,3,0,2,43,2,1,4,0,3,22,11,22,10,30,66,18,2,1,11,21,11,25,71,55,7,1,65,0,16,3,2,2,2,26,45,28,4,28,36,7,2,27,28,53,11,21,11,18,14,17,111,72,56,50,14,50,785,52,76,44,33,24,27,35,42,34,4,0,13,47,15,3,22,0,2,0,36,17,2,24,85,6,2,0,2,3,2,14,2,9,8,46,39,7,3,1,3,21,2,6,2,1,2,4,4,0,19,0,13,4,159,52,19,3,54,47,21,1,2,0,185,46,42,3,37,47,21,0,60,42,86,25,391,63,32,0,257,0,11,39,8,0,22,0,12,39,3,3,55,56,264,8,2,36,18,0,50,29,113,6,2,1,2,37,22,0,698,921,103,110,18,195,2749,1070,4050,582,8634,568,8,30,114,29,19,47,17,3,32,20,6,18,881,68,12,0,67,12,65,1,31,6124,20,754,9486,286,82,395,2309,106,6,12,4,8,8,9,5991,84,2,70,2,1,3,0,3,1,3,3,2,11,2,0,2,6,2,64,2,3,3,7,2,6,2,27,2,3,2,4,2,0,4,6,2,339,3,24,2,24,2,30,2,24,2,30,2,24,2,30,2,24,2,30,2,24,2,7,4149,196,60,67,1213,3,2,26,2,1,2,0,3,0,2,9,2,3,2,0,2,0,7,0,5,0,2,0,2,0,2,2,2,1,2,0,3,0,2,0,2,0,2,0,2,0,2,1,2,0,3,3,2,6,2,3,2,3,2,0,2,9,2,16,6,2,2,4,2,16,4421,42710,42,4148,12,221,3,5761,15,7472,3104,541] + +// eslint-disable-next-line comma-spacing +const astralIdentifierCodes = [509,0,227,0,150,4,294,9,1368,2,2,1,6,3,41,2,5,0,166,1,1306,2,54,14,32,9,16,3,46,10,54,9,7,2,37,13,2,9,52,0,13,2,49,13,10,2,4,9,83,11,7,0,161,11,6,9,7,3,57,0,2,6,3,1,3,2,10,0,11,1,3,6,4,4,193,17,10,9,87,19,13,9,214,6,3,8,28,1,83,16,16,9,82,12,9,9,84,14,5,9,423,9,280,9,41,6,2,3,9,0,10,10,47,15,406,7,2,7,17,9,57,21,2,13,123,5,4,0,2,1,2,6,2,0,9,9,19719,9,135,4,60,6,26,9,1016,45,17,3,19723,1,5319,4,4,5,9,7,3,6,31,3,149,2,1418,49,513,54,5,49,9,0,15,0,23,4,2,14,1361,6,2,16,3,6,2,1,2,4,2214,6,110,6,6,9,792487,239] + +// This has a complexity linear to the value of the code. The +// assumption is that looking up astral identifier characters is +// rare. +function isInAstralSet(code, set) { + let pos = 0x10000 + for (let i = 0; i < set.length; i += 2) { + pos += set[i] + if (pos > code) return false + pos += set[i + 1] + if (pos >= code) return true + } +} + +// Test whether a given character code starts an identifier. + +export function isIdentifierStart(code, astral) { + if (code < 65) return code === 36 + if (code < 91) return true + if (code < 97) return code === 95 + if (code < 123) return true + if (code <= 0xffff) return code >= 0xaa && nonASCIIidentifierStart.test(String.fromCharCode(code)) + if (astral === false) return false + return isInAstralSet(code, astralIdentifierStartCodes) +} + +// Test whether a given character is part of an identifier. + +export function isIdentifierChar(code, astral) { + if (code < 48) return code === 36 + if (code < 58) return true + if (code < 65) return false + if (code < 91) return true + if (code < 97) return code === 95 + if (code < 123) return true + if (code <= 0xffff) return code >= 0xaa && nonASCIIidentifier.test(String.fromCharCode(code)) + if (astral === false) return false + return isInAstralSet(code, astralIdentifierStartCodes) || isInAstralSet(code, astralIdentifierCodes) +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/index.js b/tools/tests/apps/modules/imports/links/acorn/src/index.js new file mode 100644 index 0000000000..aaf664da6b --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/index.js @@ -0,0 +1,78 @@ +// Acorn is a tiny, fast JavaScript parser written in JavaScript. +// +// Acorn was written by Marijn Haverbeke, Ingvar Stepanyan, and +// various contributors and released under an MIT license. +// +// Git repositories for Acorn are available at +// +// http://marijnhaverbeke.nl/git/acorn +// https://github.com/acornjs/acorn.git +// +// Please use the [github bug tracker][ghbt] to report issues. +// +// [ghbt]: https://github.com/acornjs/acorn/issues +// +// This file defines the main parser interface. The library also comes +// with a [error-tolerant parser][dammit] and an +// [abstract syntax tree walker][walk], defined in other files. +// +// [dammit]: acorn_loose.js +// [walk]: util/walk.js + +import {Parser} from "./state" +import "./parseutil" +import "./statement" +import "./lval" +import "./expression" +import "./location" +import "./scope" + +export {Parser, plugins} from "./state" +export {defaultOptions} from "./options" +export {Position, SourceLocation, getLineInfo} from "./locutil" +export {Node} from "./node" +export {TokenType, types as tokTypes, keywords as keywordTypes} from "./tokentype" +export {TokContext, types as tokContexts} from "./tokencontext" +export {isIdentifierChar, isIdentifierStart} from "./identifier" +export {Token} from "./tokenize" +export {isNewLine, lineBreak, lineBreakG, nonASCIIwhitespace} from "./whitespace" + +export const version = "5.5.3" + +// The main exported interface (under `self.acorn` when in the +// browser) is a `parse` function that takes a code string and +// returns an abstract syntax tree as specified by [Mozilla parser +// API][api]. +// +// [api]: https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API + +export function parse(input, options) { + return new Parser(options, input).parse() +} + +// This function tries to parse a single expression at a given +// offset in a string. Useful for parsing mixed-language formats +// that embed JavaScript expressions. + +export function parseExpressionAt(input, pos, options) { + let p = new Parser(options, input, pos) + p.nextToken() + return p.parseExpression() +} + +// Acorn is organized as a tokenizer and a recursive-descent parser. +// The `tokenizer` export provides an interface to the tokenizer. + +export function tokenizer(input, options) { + return new Parser(options, input) +} + +// This is a terrible kludge to support the existing, pre-ES6 +// interface where the loose parser module retroactively adds exports +// to this module. +export let parse_dammit, LooseParser, pluginsLoose // eslint-disable-line camelcase +export function addLooseExports(parse, Parser, plugins) { + parse_dammit = parse // eslint-disable-line camelcase + LooseParser = Parser + pluginsLoose = plugins +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/location.js b/tools/tests/apps/modules/imports/links/acorn/src/location.js new file mode 100644 index 0000000000..99e71234cc --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/location.js @@ -0,0 +1,26 @@ +import {Parser} from "./state" +import {Position, getLineInfo} from "./locutil" + +const pp = Parser.prototype + +// This function is used to raise exceptions on parse errors. It +// takes an offset integer (into the current `input`) to indicate +// the location of the error, attaches the position to the end +// of the error message, and then raises a `SyntaxError` with that +// message. + +pp.raise = function(pos, message) { + let loc = getLineInfo(this.input, pos) + message += " (" + loc.line + ":" + loc.column + ")" + let err = new SyntaxError(message) + err.pos = pos; err.loc = loc; err.raisedAt = this.pos + throw err +} + +pp.raiseRecoverable = pp.raise + +pp.curPosition = function() { + if (this.options.locations) { + return new Position(this.curLine, this.pos - this.lineStart) + } +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/locutil.js b/tools/tests/apps/modules/imports/links/acorn/src/locutil.js new file mode 100644 index 0000000000..5a9465e017 --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/locutil.js @@ -0,0 +1,42 @@ +import {lineBreakG} from "./whitespace" + +// These are used when `options.locations` is on, for the +// `startLoc` and `endLoc` properties. + +export class Position { + constructor(line, col) { + this.line = line + this.column = col + } + + offset(n) { + return new Position(this.line, this.column + n) + } +} + +export class SourceLocation { + constructor(p, start, end) { + this.start = start + this.end = end + if (p.sourceFile !== null) this.source = p.sourceFile + } +} + +// The `getLineInfo` function is mostly useful when the +// `locations` option is off (for performance reasons) and you +// want to find the line/column position for a given character +// offset. `input` should be the code string that the offset refers +// into. + +export function getLineInfo(input, offset) { + for (let line = 1, cur = 0;;) { + lineBreakG.lastIndex = cur + let match = lineBreakG.exec(input) + if (match && match.index < offset) { + ++line + cur = match.index + match[0].length + } else { + return new Position(line, offset - cur) + } + } +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/loose/expression.js b/tools/tests/apps/modules/imports/links/acorn/src/loose/expression.js new file mode 100644 index 0000000000..b6c5ddbcb2 --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/loose/expression.js @@ -0,0 +1,582 @@ +import {LooseParser} from "./state" +import {isDummy} from "./parseutil" +import {tokTypes as tt} from "../index" + +const lp = LooseParser.prototype + +lp.checkLVal = function(expr) { + if (!expr) return expr + switch (expr.type) { + case "Identifier": + case "MemberExpression": + return expr + + case "ParenthesizedExpression": + expr.expression = this.checkLVal(expr.expression) + return expr + + default: + return this.dummyIdent() + } +} + +lp.parseExpression = function(noIn) { + let start = this.storeCurrentPos() + let expr = this.parseMaybeAssign(noIn) + if (this.tok.type === tt.comma) { + let node = this.startNodeAt(start) + node.expressions = [expr] + while (this.eat(tt.comma)) node.expressions.push(this.parseMaybeAssign(noIn)) + return this.finishNode(node, "SequenceExpression") + } + return expr +} + +lp.parseParenExpression = function() { + this.pushCx() + this.expect(tt.parenL) + let val = this.parseExpression() + this.popCx() + this.expect(tt.parenR) + return val +} + +lp.parseMaybeAssign = function(noIn) { + if (this.toks.isContextual("yield")) { + let node = this.startNode() + this.next() + if (this.semicolon() || this.canInsertSemicolon() || (this.tok.type != tt.star && !this.tok.type.startsExpr)) { + node.delegate = false + node.argument = null + } else { + node.delegate = this.eat(tt.star) + node.argument = this.parseMaybeAssign() + } + return this.finishNode(node, "YieldExpression") + } + + let start = this.storeCurrentPos() + let left = this.parseMaybeConditional(noIn) + if (this.tok.type.isAssign) { + let node = this.startNodeAt(start) + node.operator = this.tok.value + node.left = this.tok.type === tt.eq ? this.toAssignable(left) : this.checkLVal(left) + this.next() + node.right = this.parseMaybeAssign(noIn) + return this.finishNode(node, "AssignmentExpression") + } + return left +} + +lp.parseMaybeConditional = function(noIn) { + let start = this.storeCurrentPos() + let expr = this.parseExprOps(noIn) + if (this.eat(tt.question)) { + let node = this.startNodeAt(start) + node.test = expr + node.consequent = this.parseMaybeAssign() + node.alternate = this.expect(tt.colon) ? this.parseMaybeAssign(noIn) : this.dummyIdent() + return this.finishNode(node, "ConditionalExpression") + } + return expr +} + +lp.parseExprOps = function(noIn) { + let start = this.storeCurrentPos() + let indent = this.curIndent, line = this.curLineStart + return this.parseExprOp(this.parseMaybeUnary(false), start, -1, noIn, indent, line) +} + +lp.parseExprOp = function(left, start, minPrec, noIn, indent, line) { + if (this.curLineStart != line && this.curIndent < indent && this.tokenStartsLine()) return left + let prec = this.tok.type.binop + if (prec != null && (!noIn || this.tok.type !== tt._in)) { + if (prec > minPrec) { + let node = this.startNodeAt(start) + node.left = left + node.operator = this.tok.value + this.next() + if (this.curLineStart != line && this.curIndent < indent && this.tokenStartsLine()) { + node.right = this.dummyIdent() + } else { + let rightStart = this.storeCurrentPos() + node.right = this.parseExprOp(this.parseMaybeUnary(false), rightStart, prec, noIn, indent, line) + } + this.finishNode(node, /&&|\|\|/.test(node.operator) ? "LogicalExpression" : "BinaryExpression") + return this.parseExprOp(node, start, minPrec, noIn, indent, line) + } + } + return left +} + +lp.parseMaybeUnary = function(sawUnary) { + let start = this.storeCurrentPos(), expr + if (this.options.ecmaVersion >= 8 && this.inAsync && this.toks.isContextual("await")) { + expr = this.parseAwait() + sawUnary = true + } else if (this.tok.type.prefix) { + let node = this.startNode(), update = this.tok.type === tt.incDec + if (!update) sawUnary = true + node.operator = this.tok.value + node.prefix = true + this.next() + node.argument = this.parseMaybeUnary(true) + if (update) node.argument = this.checkLVal(node.argument) + expr = this.finishNode(node, update ? "UpdateExpression" : "UnaryExpression") + } else if (this.tok.type === tt.ellipsis) { + let node = this.startNode() + this.next() + node.argument = this.parseMaybeUnary(sawUnary) + expr = this.finishNode(node, "SpreadElement") + } else { + expr = this.parseExprSubscripts() + while (this.tok.type.postfix && !this.canInsertSemicolon()) { + let node = this.startNodeAt(start) + node.operator = this.tok.value + node.prefix = false + node.argument = this.checkLVal(expr) + this.next() + expr = this.finishNode(node, "UpdateExpression") + } + } + + if (!sawUnary && this.eat(tt.starstar)) { + let node = this.startNodeAt(start) + node.operator = "**" + node.left = expr + node.right = this.parseMaybeUnary(false) + return this.finishNode(node, "BinaryExpression") + } + + return expr +} + +lp.parseExprSubscripts = function() { + let start = this.storeCurrentPos() + return this.parseSubscripts(this.parseExprAtom(), start, false, this.curIndent, this.curLineStart) +} + +lp.parseSubscripts = function(base, start, noCalls, startIndent, line) { + for (;;) { + if (this.curLineStart != line && this.curIndent <= startIndent && this.tokenStartsLine()) { + if (this.tok.type == tt.dot && this.curIndent == startIndent) + --startIndent + else + return base + } + + let maybeAsyncArrow = base.type === "Identifier" && base.name === "async" && !this.canInsertSemicolon() + + if (this.eat(tt.dot)) { + let node = this.startNodeAt(start) + node.object = base + if (this.curLineStart != line && this.curIndent <= startIndent && this.tokenStartsLine()) + node.property = this.dummyIdent() + else + node.property = this.parsePropertyAccessor() || this.dummyIdent() + node.computed = false + base = this.finishNode(node, "MemberExpression") + } else if (this.tok.type == tt.bracketL) { + this.pushCx() + this.next() + let node = this.startNodeAt(start) + node.object = base + node.property = this.parseExpression() + node.computed = true + this.popCx() + this.expect(tt.bracketR) + base = this.finishNode(node, "MemberExpression") + } else if (!noCalls && this.tok.type == tt.parenL) { + let exprList = this.parseExprList(tt.parenR) + if (maybeAsyncArrow && this.eat(tt.arrow)) + return this.parseArrowExpression(this.startNodeAt(start), exprList, true) + let node = this.startNodeAt(start) + node.callee = base + node.arguments = exprList + base = this.finishNode(node, "CallExpression") + } else if (this.tok.type == tt.backQuote) { + let node = this.startNodeAt(start) + node.tag = base + node.quasi = this.parseTemplate() + base = this.finishNode(node, "TaggedTemplateExpression") + } else { + return base + } + } +} + +lp.parseExprAtom = function() { + let node + switch (this.tok.type) { + case tt._this: + case tt._super: + let type = this.tok.type === tt._this ? "ThisExpression" : "Super" + node = this.startNode() + this.next() + return this.finishNode(node, type) + + case tt.name: + let start = this.storeCurrentPos() + let id = this.parseIdent() + let isAsync = false + if (id.name === "async" && !this.canInsertSemicolon()) { + if (this.eat(tt._function)) + return this.parseFunction(this.startNodeAt(start), false, true) + if (this.tok.type === tt.name) { + id = this.parseIdent() + isAsync = true + } + } + return this.eat(tt.arrow) ? this.parseArrowExpression(this.startNodeAt(start), [id], isAsync) : id + + case tt.regexp: + node = this.startNode() + let val = this.tok.value + node.regex = {pattern: val.pattern, flags: val.flags} + node.value = val.value + node.raw = this.input.slice(this.tok.start, this.tok.end) + this.next() + return this.finishNode(node, "Literal") + + case tt.num: case tt.string: + node = this.startNode() + node.value = this.tok.value + node.raw = this.input.slice(this.tok.start, this.tok.end) + this.next() + return this.finishNode(node, "Literal") + + case tt._null: case tt._true: case tt._false: + node = this.startNode() + node.value = this.tok.type === tt._null ? null : this.tok.type === tt._true + node.raw = this.tok.type.keyword + this.next() + return this.finishNode(node, "Literal") + + case tt.parenL: + let parenStart = this.storeCurrentPos() + this.next() + let inner = this.parseExpression() + this.expect(tt.parenR) + if (this.eat(tt.arrow)) { + // (a,)=>a // SequenceExpression makes dummy in the last hole. Drop the dummy. + let params = inner.expressions || [inner] + if (params.length && isDummy(params[params.length - 1])) + params.pop() + return this.parseArrowExpression(this.startNodeAt(parenStart), params) + } + if (this.options.preserveParens) { + let par = this.startNodeAt(parenStart) + par.expression = inner + inner = this.finishNode(par, "ParenthesizedExpression") + } + return inner + + case tt.bracketL: + node = this.startNode() + node.elements = this.parseExprList(tt.bracketR, true) + return this.finishNode(node, "ArrayExpression") + + case tt.braceL: + return this.parseObj() + + case tt._class: + return this.parseClass(false) + + case tt._function: + node = this.startNode() + this.next() + return this.parseFunction(node, false) + + case tt._new: + return this.parseNew() + + case tt.backQuote: + return this.parseTemplate() + + default: + return this.dummyIdent() + } +} + +lp.parseNew = function() { + let node = this.startNode(), startIndent = this.curIndent, line = this.curLineStart + let meta = this.parseIdent(true) + if (this.options.ecmaVersion >= 6 && this.eat(tt.dot)) { + node.meta = meta + node.property = this.parseIdent(true) + return this.finishNode(node, "MetaProperty") + } + let start = this.storeCurrentPos() + node.callee = this.parseSubscripts(this.parseExprAtom(), start, true, startIndent, line) + if (this.tok.type == tt.parenL) { + node.arguments = this.parseExprList(tt.parenR) + } else { + node.arguments = [] + } + return this.finishNode(node, "NewExpression") +} + +lp.parseTemplateElement = function() { + let elem = this.startNode() + + // The loose parser accepts invalid unicode escapes even in untagged templates. + if (this.tok.type === tt.invalidTemplate) { + elem.value = { + raw: this.tok.value, + cooked: null + } + } else { + elem.value = { + raw: this.input.slice(this.tok.start, this.tok.end).replace(/\r\n?/g, "\n"), + cooked: this.tok.value + } + } + this.next() + elem.tail = this.tok.type === tt.backQuote + return this.finishNode(elem, "TemplateElement") +} + +lp.parseTemplate = function() { + let node = this.startNode() + this.next() + node.expressions = [] + let curElt = this.parseTemplateElement() + node.quasis = [curElt] + while (!curElt.tail) { + this.next() + node.expressions.push(this.parseExpression()) + if (this.expect(tt.braceR)) { + curElt = this.parseTemplateElement() + } else { + curElt = this.startNode() + curElt.value = {cooked: "", raw: ""} + curElt.tail = true + this.finishNode(curElt, "TemplateElement") + } + node.quasis.push(curElt) + } + this.expect(tt.backQuote) + return this.finishNode(node, "TemplateLiteral") +} + +lp.parseObj = function() { + let node = this.startNode() + node.properties = [] + this.pushCx() + let indent = this.curIndent + 1, line = this.curLineStart + this.eat(tt.braceL) + if (this.curIndent + 1 < indent) { indent = this.curIndent; line = this.curLineStart } + while (!this.closes(tt.braceR, indent, line)) { + let prop = this.startNode(), isGenerator, isAsync, start + if (this.options.ecmaVersion >= 9 && this.eat(tt.ellipsis)) { + prop.argument = this.parseMaybeAssign() + node.properties.push(this.finishNode(prop, "SpreadElement")) + this.eat(tt.comma) + continue + } + if (this.options.ecmaVersion >= 6) { + start = this.storeCurrentPos() + prop.method = false + prop.shorthand = false + isGenerator = this.eat(tt.star) + } + this.parsePropertyName(prop) + if (this.toks.isAsyncProp(prop)) { + isAsync = true + isGenerator = this.options.ecmaVersion >= 9 && this.eat(tt.star) + this.parsePropertyName(prop) + } else { + isAsync = false + } + if (isDummy(prop.key)) { if (isDummy(this.parseMaybeAssign())) this.next(); this.eat(tt.comma); continue } + if (this.eat(tt.colon)) { + prop.kind = "init" + prop.value = this.parseMaybeAssign() + } else if (this.options.ecmaVersion >= 6 && (this.tok.type === tt.parenL || this.tok.type === tt.braceL)) { + prop.kind = "init" + prop.method = true + prop.value = this.parseMethod(isGenerator, isAsync) + } else if (this.options.ecmaVersion >= 5 && prop.key.type === "Identifier" && + !prop.computed && (prop.key.name === "get" || prop.key.name === "set") && + (this.tok.type != tt.comma && this.tok.type != tt.braceR && this.tok.type != tt.eq)) { + prop.kind = prop.key.name + this.parsePropertyName(prop) + prop.value = this.parseMethod(false) + } else { + prop.kind = "init" + if (this.options.ecmaVersion >= 6) { + if (this.eat(tt.eq)) { + let assign = this.startNodeAt(start) + assign.operator = "=" + assign.left = prop.key + assign.right = this.parseMaybeAssign() + prop.value = this.finishNode(assign, "AssignmentExpression") + } else { + prop.value = prop.key + } + } else { + prop.value = this.dummyIdent() + } + prop.shorthand = true + } + node.properties.push(this.finishNode(prop, "Property")) + this.eat(tt.comma) + } + this.popCx() + if (!this.eat(tt.braceR)) { + // If there is no closing brace, make the node span to the start + // of the next token (this is useful for Tern) + this.last.end = this.tok.start + if (this.options.locations) this.last.loc.end = this.tok.loc.start + } + return this.finishNode(node, "ObjectExpression") +} + +lp.parsePropertyName = function(prop) { + if (this.options.ecmaVersion >= 6) { + if (this.eat(tt.bracketL)) { + prop.computed = true + prop.key = this.parseExpression() + this.expect(tt.bracketR) + return + } else { + prop.computed = false + } + } + let key = (this.tok.type === tt.num || this.tok.type === tt.string) ? this.parseExprAtom() : this.parseIdent() + prop.key = key || this.dummyIdent() +} + +lp.parsePropertyAccessor = function() { + if (this.tok.type === tt.name || this.tok.type.keyword) return this.parseIdent() +} + +lp.parseIdent = function() { + let name = this.tok.type === tt.name ? this.tok.value : this.tok.type.keyword + if (!name) return this.dummyIdent() + let node = this.startNode() + this.next() + node.name = name + return this.finishNode(node, "Identifier") +} + +lp.initFunction = function(node) { + node.id = null + node.params = [] + if (this.options.ecmaVersion >= 6) { + node.generator = false + node.expression = false + } + if (this.options.ecmaVersion >= 8) + node.async = false +} + +// Convert existing expression atom to assignable pattern +// if possible. + +lp.toAssignable = function(node, binding) { + if (!node || node.type == "Identifier" || (node.type == "MemberExpression" && !binding)) { + // Okay + } else if (node.type == "ParenthesizedExpression") { + this.toAssignable(node.expression, binding) + } else if (this.options.ecmaVersion < 6) { + return this.dummyIdent() + } else if (node.type == "ObjectExpression") { + node.type = "ObjectPattern" + for (let prop of node.properties) + this.toAssignable(prop, binding) + } else if (node.type == "ArrayExpression") { + node.type = "ArrayPattern" + this.toAssignableList(node.elements, binding) + } else if (node.type == "Property") { + this.toAssignable(node.value, binding) + } else if (node.type == "SpreadElement") { + node.type = "RestElement" + this.toAssignable(node.argument, binding) + } else if (node.type == "AssignmentExpression") { + node.type = "AssignmentPattern" + delete node.operator + } else { + return this.dummyIdent() + } + return node +} + +lp.toAssignableList = function(exprList, binding) { + for (let expr of exprList) + this.toAssignable(expr, binding) + return exprList +} + +lp.parseFunctionParams = function(params) { + params = this.parseExprList(tt.parenR) + return this.toAssignableList(params, true) +} + +lp.parseMethod = function(isGenerator, isAsync) { + let node = this.startNode(), oldInAsync = this.inAsync + this.initFunction(node) + if (this.options.ecmaVersion >= 6) + node.generator = !!isGenerator + if (this.options.ecmaVersion >= 8) + node.async = !!isAsync + this.inAsync = node.async + node.params = this.parseFunctionParams() + node.body = this.parseBlock() + this.toks.adaptDirectivePrologue(node.body.body) + this.inAsync = oldInAsync + return this.finishNode(node, "FunctionExpression") +} + +lp.parseArrowExpression = function(node, params, isAsync) { + let oldInAsync = this.inAsync + this.initFunction(node) + if (this.options.ecmaVersion >= 8) + node.async = !!isAsync + this.inAsync = node.async + node.params = this.toAssignableList(params, true) + node.expression = this.tok.type !== tt.braceL + if (node.expression) { + node.body = this.parseMaybeAssign() + } else { + node.body = this.parseBlock() + this.toks.adaptDirectivePrologue(node.body.body) + } + this.inAsync = oldInAsync + return this.finishNode(node, "ArrowFunctionExpression") +} + +lp.parseExprList = function(close, allowEmpty) { + this.pushCx() + let indent = this.curIndent, line = this.curLineStart, elts = [] + this.next() // Opening bracket + while (!this.closes(close, indent + 1, line)) { + if (this.eat(tt.comma)) { + elts.push(allowEmpty ? null : this.dummyIdent()) + continue + } + let elt = this.parseMaybeAssign() + if (isDummy(elt)) { + if (this.closes(close, indent, line)) break + this.next() + } else { + elts.push(elt) + } + this.eat(tt.comma) + } + this.popCx() + if (!this.eat(close)) { + // If there is no closing brace, make the node span to the start + // of the next token (this is useful for Tern) + this.last.end = this.tok.start + if (this.options.locations) this.last.loc.end = this.tok.loc.start + } + return elts +} + +lp.parseAwait = function() { + let node = this.startNode() + this.next() + node.argument = this.parseMaybeUnary() + return this.finishNode(node, "AwaitExpression") +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/loose/index.js b/tools/tests/apps/modules/imports/links/acorn/src/loose/index.js new file mode 100644 index 0000000000..a698061a8b --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/loose/index.js @@ -0,0 +1,47 @@ +// Acorn: Loose parser +// +// This module provides an alternative parser (`parse_dammit`) that +// exposes that same interface as `parse`, but will try to parse +// anything as JavaScript, repairing syntax error the best it can. +// There are circumstances in which it will raise an error and give +// up, but they are very rare. The resulting AST will be a mostly +// valid JavaScript AST (as per the [Mozilla parser API][api], except +// that: +// +// - Return outside functions is allowed +// +// - Label consistency (no conflicts, break only to existing labels) +// is not enforced. +// +// - Bogus Identifier nodes with a name of `"✖"` are inserted whenever +// the parser got too confused to return anything meaningful. +// +// [api]: https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API +// +// The expected use for this is to *first* try `acorn.parse`, and only +// if that fails switch to `parse_dammit`. The loose parser might +// parse badly indented code incorrectly, so **don't** use it as +// your default parser. +// +// Quite a lot of acorn.js is duplicated here. The alternative was to +// add a *lot* of extra cruft to that file, making it less readable +// and slower. Copying and editing the code allowed me to make +// invasive changes and simplifications without creating a complicated +// tangle. + +import {addLooseExports, defaultOptions} from "../index" +import {LooseParser, pluginsLoose} from "./state" +import "./tokenize" +import "./statement" +import "./expression" + +export {LooseParser, pluginsLoose} from "./state" + +defaultOptions.tabSize = 4 + +// eslint-disable-next-line camelcase +export function parse_dammit(input, options) { + return new LooseParser(input, options).parse() +} + +addLooseExports(parse_dammit, LooseParser, pluginsLoose) diff --git a/tools/tests/apps/modules/imports/links/acorn/src/loose/parseutil.js b/tools/tests/apps/modules/imports/links/acorn/src/loose/parseutil.js new file mode 100644 index 0000000000..b620fdaf0a --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/loose/parseutil.js @@ -0,0 +1 @@ +export function isDummy(node) { return node.name == "✖" } diff --git a/tools/tests/apps/modules/imports/links/acorn/src/loose/state.js b/tools/tests/apps/modules/imports/links/acorn/src/loose/state.js new file mode 100644 index 0000000000..da068edb51 --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/loose/state.js @@ -0,0 +1,170 @@ +import {tokenizer, SourceLocation, tokTypes as tt, Node, lineBreak, isNewLine} from "../index" + +function noop() {} + +// Registered plugins +export const pluginsLoose = {} + +export class LooseParser { + constructor(input, options = {}) { + this.toks = tokenizer(input, options) + this.options = this.toks.options + this.input = this.toks.input + this.tok = this.last = {type: tt.eof, start: 0, end: 0} + this.tok.validateRegExpFlags = noop + this.tok.validateRegExpPattern = noop + if (this.options.locations) { + let here = this.toks.curPosition() + this.tok.loc = new SourceLocation(this.toks, here, here) + } + this.ahead = [] // Tokens ahead + this.context = [] // Indentation contexted + this.curIndent = 0 + this.curLineStart = 0 + this.nextLineStart = this.lineEnd(this.curLineStart) + 1 + this.inAsync = false + // Load plugins + this.options.pluginsLoose = options.pluginsLoose || {} + this.loadPlugins(this.options.pluginsLoose) + } + + startNode() { + return new Node(this.toks, this.tok.start, this.options.locations ? this.tok.loc.start : null) + } + + storeCurrentPos() { + return this.options.locations ? [this.tok.start, this.tok.loc.start] : this.tok.start + } + + startNodeAt(pos) { + if (this.options.locations) { + return new Node(this.toks, pos[0], pos[1]) + } else { + return new Node(this.toks, pos) + } + } + + finishNode(node, type) { + node.type = type + node.end = this.last.end + if (this.options.locations) + node.loc.end = this.last.loc.end + if (this.options.ranges) + node.range[1] = this.last.end + return node + } + + dummyNode(type) { + let dummy = this.startNode() + dummy.type = type + dummy.end = dummy.start + if (this.options.locations) + dummy.loc.end = dummy.loc.start + if (this.options.ranges) + dummy.range[1] = dummy.start + this.last = {type: tt.name, start: dummy.start, end: dummy.start, loc: dummy.loc} + return dummy + } + + dummyIdent() { + let dummy = this.dummyNode("Identifier") + dummy.name = "✖" + return dummy + } + + dummyString() { + let dummy = this.dummyNode("Literal") + dummy.value = dummy.raw = "✖" + return dummy + } + + eat(type) { + if (this.tok.type === type) { + this.next() + return true + } else { + return false + } + } + + isContextual(name) { + return this.tok.type === tt.name && this.tok.value === name + } + + eatContextual(name) { + return this.tok.value === name && this.eat(tt.name) + } + + canInsertSemicolon() { + return this.tok.type === tt.eof || this.tok.type === tt.braceR || + lineBreak.test(this.input.slice(this.last.end, this.tok.start)) + } + + semicolon() { + return this.eat(tt.semi) + } + + expect(type) { + if (this.eat(type)) return true + for (let i = 1; i <= 2; i++) { + if (this.lookAhead(i).type == type) { + for (let j = 0; j < i; j++) this.next() + return true + } + } + } + + pushCx() { + this.context.push(this.curIndent) + } + + popCx() { + this.curIndent = this.context.pop() + } + + lineEnd(pos) { + while (pos < this.input.length && !isNewLine(this.input.charCodeAt(pos))) ++pos + return pos + } + + indentationAfter(pos) { + for (let count = 0;; ++pos) { + let ch = this.input.charCodeAt(pos) + if (ch === 32) ++count + else if (ch === 9) count += this.options.tabSize + else return count + } + } + + closes(closeTok, indent, line, blockHeuristic) { + if (this.tok.type === closeTok || this.tok.type === tt.eof) return true + return line != this.curLineStart && this.curIndent < indent && this.tokenStartsLine() && + (!blockHeuristic || this.nextLineStart >= this.input.length || + this.indentationAfter(this.nextLineStart) < indent) + } + + tokenStartsLine() { + for (let p = this.tok.start - 1; p >= this.curLineStart; --p) { + let ch = this.input.charCodeAt(p) + if (ch !== 9 && ch !== 32) return false + } + return true + } + + extend(name, f) { + this[name] = f(this[name]) + } + + loadPlugins(pluginConfigs) { + for (let name in pluginConfigs) { + let plugin = pluginsLoose[name] + if (!plugin) throw new Error("Plugin '" + name + "' not found") + plugin(this, pluginConfigs[name]) + } + } + + parse() { + this.next() + return this.parseTopLevel() + } +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/loose/statement.js b/tools/tests/apps/modules/imports/links/acorn/src/loose/statement.js new file mode 100644 index 0000000000..aa30696c41 --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/loose/statement.js @@ -0,0 +1,459 @@ +import {LooseParser} from "./state" +import {isDummy} from "./parseutil" +import {getLineInfo, tokTypes as tt} from "../index" + +const lp = LooseParser.prototype + +lp.parseTopLevel = function() { + let node = this.startNodeAt(this.options.locations ? [0, getLineInfo(this.input, 0)] : 0) + node.body = [] + while (this.tok.type !== tt.eof) node.body.push(this.parseStatement()) + this.toks.adaptDirectivePrologue(node.body) + this.last = this.tok + if (this.options.ecmaVersion >= 6) { + node.sourceType = this.options.sourceType + } + return this.finishNode(node, "Program") +} + +lp.parseStatement = function() { + let starttype = this.tok.type, node = this.startNode(), kind + + if (this.toks.isLet()) { + starttype = tt._var + kind = "let" + } + + switch (starttype) { + case tt._break: case tt._continue: + this.next() + let isBreak = starttype === tt._break + if (this.semicolon() || this.canInsertSemicolon()) { + node.label = null + } else { + node.label = this.tok.type === tt.name ? this.parseIdent() : null + this.semicolon() + } + return this.finishNode(node, isBreak ? "BreakStatement" : "ContinueStatement") + + case tt._debugger: + this.next() + this.semicolon() + return this.finishNode(node, "DebuggerStatement") + + case tt._do: + this.next() + node.body = this.parseStatement() + node.test = this.eat(tt._while) ? this.parseParenExpression() : this.dummyIdent() + this.semicolon() + return this.finishNode(node, "DoWhileStatement") + + case tt._for: + this.next() // `for` keyword + let isAwait = this.options.ecmaVersion >= 9 && this.inAsync && this.eatContextual("await") + + this.pushCx() + this.expect(tt.parenL) + if (this.tok.type === tt.semi) return this.parseFor(node, null) + let isLet = this.toks.isLet() + if (isLet || this.tok.type === tt._var || this.tok.type === tt._const) { + let init = this.parseVar(true, isLet ? "let" : this.tok.value) + if (init.declarations.length === 1 && (this.tok.type === tt._in || this.isContextual("of"))) { + if (this.options.ecmaVersion >= 9 && this.tok.type !== tt._in) { + node.await = isAwait + } + return this.parseForIn(node, init) + } + return this.parseFor(node, init) + } + let init = this.parseExpression(true) + if (this.tok.type === tt._in || this.isContextual("of")) { + if (this.options.ecmaVersion >= 9 && this.tok.type !== tt._in) { + node.await = isAwait + } + return this.parseForIn(node, this.toAssignable(init)) + } + return this.parseFor(node, init) + + case tt._function: + this.next() + return this.parseFunction(node, true) + + case tt._if: + this.next() + node.test = this.parseParenExpression() + node.consequent = this.parseStatement() + node.alternate = this.eat(tt._else) ? this.parseStatement() : null + return this.finishNode(node, "IfStatement") + + case tt._return: + this.next() + if (this.eat(tt.semi) || this.canInsertSemicolon()) node.argument = null + else { node.argument = this.parseExpression(); this.semicolon() } + return this.finishNode(node, "ReturnStatement") + + case tt._switch: + let blockIndent = this.curIndent, line = this.curLineStart + this.next() + node.discriminant = this.parseParenExpression() + node.cases = [] + this.pushCx() + this.expect(tt.braceL) + + let cur + while (!this.closes(tt.braceR, blockIndent, line, true)) { + if (this.tok.type === tt._case || this.tok.type === tt._default) { + let isCase = this.tok.type === tt._case + if (cur) this.finishNode(cur, "SwitchCase") + node.cases.push(cur = this.startNode()) + cur.consequent = [] + this.next() + if (isCase) cur.test = this.parseExpression() + else cur.test = null + this.expect(tt.colon) + } else { + if (!cur) { + node.cases.push(cur = this.startNode()) + cur.consequent = [] + cur.test = null + } + cur.consequent.push(this.parseStatement()) + } + } + if (cur) this.finishNode(cur, "SwitchCase") + this.popCx() + this.eat(tt.braceR) + return this.finishNode(node, "SwitchStatement") + + case tt._throw: + this.next() + node.argument = this.parseExpression() + this.semicolon() + return this.finishNode(node, "ThrowStatement") + + case tt._try: + this.next() + node.block = this.parseBlock() + node.handler = null + if (this.tok.type === tt._catch) { + let clause = this.startNode() + this.next() + this.expect(tt.parenL) + clause.param = this.toAssignable(this.parseExprAtom(), true) + this.expect(tt.parenR) + clause.body = this.parseBlock() + node.handler = this.finishNode(clause, "CatchClause") + } + node.finalizer = this.eat(tt._finally) ? this.parseBlock() : null + if (!node.handler && !node.finalizer) return node.block + return this.finishNode(node, "TryStatement") + + case tt._var: + case tt._const: + return this.parseVar(false, kind || this.tok.value) + + case tt._while: + this.next() + node.test = this.parseParenExpression() + node.body = this.parseStatement() + return this.finishNode(node, "WhileStatement") + + case tt._with: + this.next() + node.object = this.parseParenExpression() + node.body = this.parseStatement() + return this.finishNode(node, "WithStatement") + + case tt.braceL: + return this.parseBlock() + + case tt.semi: + this.next() + return this.finishNode(node, "EmptyStatement") + + case tt._class: + return this.parseClass(true) + + case tt._import: + return this.parseImport() + + case tt._export: + return this.parseExport() + + default: + if (this.toks.isAsyncFunction()) { + this.next() + this.next() + return this.parseFunction(node, true, true) + } + let expr = this.parseExpression() + if (isDummy(expr)) { + this.next() + if (this.tok.type === tt.eof) return this.finishNode(node, "EmptyStatement") + return this.parseStatement() + } else if (starttype === tt.name && expr.type === "Identifier" && this.eat(tt.colon)) { + node.body = this.parseStatement() + node.label = expr + return this.finishNode(node, "LabeledStatement") + } else { + node.expression = expr + this.semicolon() + return this.finishNode(node, "ExpressionStatement") + } + } +} + +lp.parseBlock = function() { + let node = this.startNode() + this.pushCx() + this.expect(tt.braceL) + let blockIndent = this.curIndent, line = this.curLineStart + node.body = [] + while (!this.closes(tt.braceR, blockIndent, line, true)) + node.body.push(this.parseStatement()) + this.popCx() + this.eat(tt.braceR) + return this.finishNode(node, "BlockStatement") +} + +lp.parseFor = function(node, init) { + node.init = init + node.test = node.update = null + if (this.eat(tt.semi) && this.tok.type !== tt.semi) node.test = this.parseExpression() + if (this.eat(tt.semi) && this.tok.type !== tt.parenR) node.update = this.parseExpression() + this.popCx() + this.expect(tt.parenR) + node.body = this.parseStatement() + return this.finishNode(node, "ForStatement") +} + +lp.parseForIn = function(node, init) { + let type = this.tok.type === tt._in ? "ForInStatement" : "ForOfStatement" + this.next() + node.left = init + node.right = this.parseExpression() + this.popCx() + this.expect(tt.parenR) + node.body = this.parseStatement() + return this.finishNode(node, type) +} + +lp.parseVar = function(noIn, kind) { + let node = this.startNode() + node.kind = kind + this.next() + node.declarations = [] + do { + let decl = this.startNode() + decl.id = this.options.ecmaVersion >= 6 ? this.toAssignable(this.parseExprAtom(), true) : this.parseIdent() + decl.init = this.eat(tt.eq) ? this.parseMaybeAssign(noIn) : null + node.declarations.push(this.finishNode(decl, "VariableDeclarator")) + } while (this.eat(tt.comma)) + if (!node.declarations.length) { + let decl = this.startNode() + decl.id = this.dummyIdent() + node.declarations.push(this.finishNode(decl, "VariableDeclarator")) + } + if (!noIn) this.semicolon() + return this.finishNode(node, "VariableDeclaration") +} + +lp.parseClass = function(isStatement) { + let node = this.startNode() + this.next() + if (this.tok.type === tt.name) node.id = this.parseIdent() + else if (isStatement === true) node.id = this.dummyIdent() + else node.id = null + node.superClass = this.eat(tt._extends) ? this.parseExpression() : null + node.body = this.startNode() + node.body.body = [] + this.pushCx() + let indent = this.curIndent + 1, line = this.curLineStart + this.eat(tt.braceL) + if (this.curIndent + 1 < indent) { indent = this.curIndent; line = this.curLineStart } + while (!this.closes(tt.braceR, indent, line)) { + if (this.semicolon()) continue + let method = this.startNode(), isGenerator, isAsync + if (this.options.ecmaVersion >= 6) { + method.static = false + isGenerator = this.eat(tt.star) + } + this.parsePropertyName(method) + if (isDummy(method.key)) { if (isDummy(this.parseMaybeAssign())) this.next(); this.eat(tt.comma); continue } + if (method.key.type === "Identifier" && !method.computed && method.key.name === "static" && + (this.tok.type != tt.parenL && this.tok.type != tt.braceL)) { + method.static = true + isGenerator = this.eat(tt.star) + this.parsePropertyName(method) + } else { + method.static = false + } + if (!method.computed && + method.key.type === "Identifier" && method.key.name === "async" && this.tok.type !== tt.parenL && + !this.canInsertSemicolon()) { + isAsync = true + isGenerator = this.options.ecmaVersion >= 9 && this.eat(tt.star) + this.parsePropertyName(method) + } else { + isAsync = false + } + if (this.options.ecmaVersion >= 5 && method.key.type === "Identifier" && + !method.computed && (method.key.name === "get" || method.key.name === "set") && + this.tok.type !== tt.parenL && this.tok.type !== tt.braceL) { + method.kind = method.key.name + this.parsePropertyName(method) + method.value = this.parseMethod(false) + } else { + if (!method.computed && !method.static && !isGenerator && !isAsync && ( + method.key.type === "Identifier" && method.key.name === "constructor" || + method.key.type === "Literal" && method.key.value === "constructor")) { + method.kind = "constructor" + } else { + method.kind = "method" + } + method.value = this.parseMethod(isGenerator, isAsync) + } + node.body.body.push(this.finishNode(method, "MethodDefinition")) + } + this.popCx() + if (!this.eat(tt.braceR)) { + // If there is no closing brace, make the node span to the start + // of the next token (this is useful for Tern) + this.last.end = this.tok.start + if (this.options.locations) this.last.loc.end = this.tok.loc.start + } + this.semicolon() + this.finishNode(node.body, "ClassBody") + return this.finishNode(node, isStatement ? "ClassDeclaration" : "ClassExpression") +} + +lp.parseFunction = function(node, isStatement, isAsync) { + let oldInAsync = this.inAsync + this.initFunction(node) + if (this.options.ecmaVersion >= 6) { + node.generator = this.eat(tt.star) + } + if (this.options.ecmaVersion >= 8) { + node.async = !!isAsync + } + if (this.tok.type === tt.name) node.id = this.parseIdent() + else if (isStatement === true) node.id = this.dummyIdent() + this.inAsync = node.async + node.params = this.parseFunctionParams() + node.body = this.parseBlock() + this.toks.adaptDirectivePrologue(node.body.body) + this.inAsync = oldInAsync + return this.finishNode(node, isStatement ? "FunctionDeclaration" : "FunctionExpression") +} + +lp.parseExport = function() { + let node = this.startNode() + this.next() + if (this.eat(tt.star)) { + node.source = this.eatContextual("from") ? this.parseExprAtom() : this.dummyString() + return this.finishNode(node, "ExportAllDeclaration") + } + if (this.eat(tt._default)) { + // export default (function foo() {}) // This is FunctionExpression. + let isAsync + if (this.tok.type === tt._function || (isAsync = this.toks.isAsyncFunction())) { + let fNode = this.startNode() + this.next() + if (isAsync) this.next() + node.declaration = this.parseFunction(fNode, "nullableID", isAsync) + } else if (this.tok.type === tt._class) { + node.declaration = this.parseClass("nullableID") + } else { + node.declaration = this.parseMaybeAssign() + this.semicolon() + } + return this.finishNode(node, "ExportDefaultDeclaration") + } + if (this.tok.type.keyword || this.toks.isLet() || this.toks.isAsyncFunction()) { + node.declaration = this.parseStatement() + node.specifiers = [] + node.source = null + } else { + node.declaration = null + node.specifiers = this.parseExportSpecifierList() + node.source = this.eatContextual("from") ? this.parseExprAtom() : null + this.semicolon() + } + return this.finishNode(node, "ExportNamedDeclaration") +} + +lp.parseImport = function() { + let node = this.startNode() + this.next() + if (this.tok.type === tt.string) { + node.specifiers = [] + node.source = this.parseExprAtom() + } else { + let elt + if (this.tok.type === tt.name && this.tok.value !== "from") { + elt = this.startNode() + elt.local = this.parseIdent() + this.finishNode(elt, "ImportDefaultSpecifier") + this.eat(tt.comma) + } + node.specifiers = this.parseImportSpecifierList() + node.source = this.eatContextual("from") && this.tok.type == tt.string ? this.parseExprAtom() : this.dummyString() + if (elt) node.specifiers.unshift(elt) + } + this.semicolon() + return this.finishNode(node, "ImportDeclaration") +} + +lp.parseImportSpecifierList = function() { + let elts = [] + if (this.tok.type === tt.star) { + let elt = this.startNode() + this.next() + elt.local = this.eatContextual("as") ? this.parseIdent() : this.dummyIdent() + elts.push(this.finishNode(elt, "ImportNamespaceSpecifier")) + } else { + let indent = this.curIndent, line = this.curLineStart, continuedLine = this.nextLineStart + this.pushCx() + this.eat(tt.braceL) + if (this.curLineStart > continuedLine) continuedLine = this.curLineStart + while (!this.closes(tt.braceR, indent + (this.curLineStart <= continuedLine ? 1 : 0), line)) { + let elt = this.startNode() + if (this.eat(tt.star)) { + elt.local = this.eatContextual("as") ? this.parseIdent() : this.dummyIdent() + this.finishNode(elt, "ImportNamespaceSpecifier") + } else { + if (this.isContextual("from")) break + elt.imported = this.parseIdent() + if (isDummy(elt.imported)) break + elt.local = this.eatContextual("as") ? this.parseIdent() : elt.imported + this.finishNode(elt, "ImportSpecifier") + } + elts.push(elt) + this.eat(tt.comma) + } + this.eat(tt.braceR) + this.popCx() + } + return elts +} + +lp.parseExportSpecifierList = function() { + let elts = [] + let indent = this.curIndent, line = this.curLineStart, continuedLine = this.nextLineStart + this.pushCx() + this.eat(tt.braceL) + if (this.curLineStart > continuedLine) continuedLine = this.curLineStart + while (!this.closes(tt.braceR, indent + (this.curLineStart <= continuedLine ? 1 : 0), line)) { + if (this.isContextual("from")) break + let elt = this.startNode() + elt.local = this.parseIdent() + if (isDummy(elt.local)) break + elt.exported = this.eatContextual("as") ? this.parseIdent() : elt.local + this.finishNode(elt, "ExportSpecifier") + elts.push(elt) + this.eat(tt.comma) + } + this.eat(tt.braceR) + this.popCx() + return elts +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/loose/tokenize.js b/tools/tests/apps/modules/imports/links/acorn/src/loose/tokenize.js new file mode 100644 index 0000000000..619b6ee9e4 --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/loose/tokenize.js @@ -0,0 +1,111 @@ +import {tokTypes as tt, Token, isNewLine, SourceLocation, getLineInfo, lineBreakG} from "../index" +import {LooseParser} from "./state" + +const lp = LooseParser.prototype + +function isSpace(ch) { + return (ch < 14 && ch > 8) || ch === 32 || ch === 160 || isNewLine(ch) +} + +lp.next = function() { + this.last = this.tok + if (this.ahead.length) + this.tok = this.ahead.shift() + else + this.tok = this.readToken() + + if (this.tok.start >= this.nextLineStart) { + while (this.tok.start >= this.nextLineStart) { + this.curLineStart = this.nextLineStart + this.nextLineStart = this.lineEnd(this.curLineStart) + 1 + } + this.curIndent = this.indentationAfter(this.curLineStart) + } +} + +lp.readToken = function() { + for (;;) { + try { + this.toks.next() + if (this.toks.type === tt.dot && + this.input.substr(this.toks.end, 1) === "." && + this.options.ecmaVersion >= 6) { + this.toks.end++ + this.toks.type = tt.ellipsis + } + return new Token(this.toks) + } catch (e) { + if (!(e instanceof SyntaxError)) throw e + + // Try to skip some text, based on the error message, and then continue + let msg = e.message, pos = e.raisedAt, replace = true + if (/unterminated/i.test(msg)) { + pos = this.lineEnd(e.pos + 1) + if (/string/.test(msg)) { + replace = {start: e.pos, end: pos, type: tt.string, value: this.input.slice(e.pos + 1, pos)} + } else if (/regular expr/i.test(msg)) { + let re = this.input.slice(e.pos, pos) + try { re = new RegExp(re) } catch (e) { /* ignore compilation error due to new syntax */ } + replace = {start: e.pos, end: pos, type: tt.regexp, value: re} + } else if (/template/.test(msg)) { + replace = { + start: e.pos, + end: pos, + type: tt.template, + value: this.input.slice(e.pos, pos) + } + } else { + replace = false + } + } else if (/invalid (unicode|regexp|number)|expecting unicode|octal literal|is reserved|directly after number|expected number in radix/i.test(msg)) { + while (pos < this.input.length && !isSpace(this.input.charCodeAt(pos))) ++pos + } else if (/character escape|expected hexadecimal/i.test(msg)) { + while (pos < this.input.length) { + let ch = this.input.charCodeAt(pos++) + if (ch === 34 || ch === 39 || isNewLine(ch)) break + } + } else if (/unexpected character/i.test(msg)) { + pos++ + replace = false + } else if (/regular expression/i.test(msg)) { + replace = true + } else { + throw e + } + this.resetTo(pos) + if (replace === true) replace = {start: pos, end: pos, type: tt.name, value: "✖"} + if (replace) { + if (this.options.locations) + replace.loc = new SourceLocation( + this.toks, + getLineInfo(this.input, replace.start), + getLineInfo(this.input, replace.end)) + return replace + } + } + } +} + +lp.resetTo = function(pos) { + this.toks.pos = pos + let ch = this.input.charAt(pos - 1) + this.toks.exprAllowed = !ch || /[[{(,;:?/*=+\-~!|&%^<>]/.test(ch) || + /[enwfd]/.test(ch) && + /\b(case|else|return|throw|new|in|(instance|type)?of|delete|void)$/.test(this.input.slice(pos - 10, pos)) + + if (this.options.locations) { + this.toks.curLine = 1 + this.toks.lineStart = lineBreakG.lastIndex = 0 + let match + while ((match = lineBreakG.exec(this.input)) && match.index < pos) { + ++this.toks.curLine + this.toks.lineStart = match.index + match[0].length + } + } +} + +lp.lookAhead = function(n) { + while (n > this.ahead.length) + this.ahead.push(this.readToken()) + return this.ahead[n - 1] +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/lval.js b/tools/tests/apps/modules/imports/links/acorn/src/lval.js new file mode 100644 index 0000000000..0fc1bc5284 --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/lval.js @@ -0,0 +1,248 @@ +import {types as tt} from "./tokentype" +import {Parser} from "./state" +import {has} from "./util" + +const pp = Parser.prototype + +// Convert existing expression atom to assignable pattern +// if possible. + +pp.toAssignable = function(node, isBinding, refDestructuringErrors) { + if (this.options.ecmaVersion >= 6 && node) { + switch (node.type) { + case "Identifier": + if (this.inAsync && node.name === "await") + this.raise(node.start, "Can not use 'await' as identifier inside an async function") + break + + case "ObjectPattern": + case "ArrayPattern": + case "RestElement": + break + + case "ObjectExpression": + node.type = "ObjectPattern" + if (refDestructuringErrors) this.checkPatternErrors(refDestructuringErrors, true) + for (let prop of node.properties) { + this.toAssignable(prop, isBinding) + // Early error: + // AssignmentRestProperty[Yield, Await] : + // `...` DestructuringAssignmentTarget[Yield, Await] + // + // It is a Syntax Error if |DestructuringAssignmentTarget| is an |ArrayLiteral| or an |ObjectLiteral|. + if ( + prop.type === "RestElement" && + (prop.argument.type === "ArrayPattern" || prop.argument.type === "ObjectPattern") + ) { + this.raise(prop.argument.start, "Unexpected token") + } + } + break + + case "Property": + // AssignmentProperty has type == "Property" + if (node.kind !== "init") this.raise(node.key.start, "Object pattern can't contain getter or setter") + this.toAssignable(node.value, isBinding) + break + + case "ArrayExpression": + node.type = "ArrayPattern" + if (refDestructuringErrors) this.checkPatternErrors(refDestructuringErrors, true) + this.toAssignableList(node.elements, isBinding) + break + + case "SpreadElement": + node.type = "RestElement" + this.toAssignable(node.argument, isBinding) + if (node.argument.type === "AssignmentPattern") + this.raise(node.argument.start, "Rest elements cannot have a default value") + break + + case "AssignmentExpression": + if (node.operator !== "=") this.raise(node.left.end, "Only '=' operator can be used for specifying default value.") + node.type = "AssignmentPattern" + delete node.operator + this.toAssignable(node.left, isBinding) + // falls through to AssignmentPattern + + case "AssignmentPattern": + break + + case "ParenthesizedExpression": + this.toAssignable(node.expression, isBinding) + break + + case "MemberExpression": + if (!isBinding) break + + default: + this.raise(node.start, "Assigning to rvalue") + } + } else if (refDestructuringErrors) this.checkPatternErrors(refDestructuringErrors, true) + return node +} + +// Convert list of expression atoms to binding list. + +pp.toAssignableList = function(exprList, isBinding) { + let end = exprList.length + for (let i = 0; i < end; i++) { + let elt = exprList[i] + if (elt) this.toAssignable(elt, isBinding) + } + if (end) { + let last = exprList[end - 1] + if (this.options.ecmaVersion === 6 && isBinding && last && last.type === "RestElement" && last.argument.type !== "Identifier") + this.unexpected(last.argument.start) + } + return exprList +} + +// Parses spread element. + +pp.parseSpread = function(refDestructuringErrors) { + let node = this.startNode() + this.next() + node.argument = this.parseMaybeAssign(false, refDestructuringErrors) + return this.finishNode(node, "SpreadElement") +} + +pp.parseRestBinding = function() { + let node = this.startNode() + this.next() + + // RestElement inside of a function parameter must be an identifier + if (this.options.ecmaVersion === 6 && this.type !== tt.name) + this.unexpected() + + node.argument = this.parseBindingAtom() + + return this.finishNode(node, "RestElement") +} + +// Parses lvalue (assignable) atom. + +pp.parseBindingAtom = function() { + if (this.options.ecmaVersion >= 6) { + switch (this.type) { + case tt.bracketL: + let node = this.startNode() + this.next() + node.elements = this.parseBindingList(tt.bracketR, true, true) + return this.finishNode(node, "ArrayPattern") + + case tt.braceL: + return this.parseObj(true) + } + } + return this.parseIdent() +} + +pp.parseBindingList = function(close, allowEmpty, allowTrailingComma) { + let elts = [], first = true + while (!this.eat(close)) { + if (first) first = false + else this.expect(tt.comma) + if (allowEmpty && this.type === tt.comma) { + elts.push(null) + } else if (allowTrailingComma && this.afterTrailingComma(close)) { + break + } else if (this.type === tt.ellipsis) { + let rest = this.parseRestBinding() + this.parseBindingListItem(rest) + elts.push(rest) + if (this.type === tt.comma) this.raise(this.start, "Comma is not permitted after the rest element") + this.expect(close) + break + } else { + let elem = this.parseMaybeDefault(this.start, this.startLoc) + this.parseBindingListItem(elem) + elts.push(elem) + } + } + return elts +} + +pp.parseBindingListItem = function(param) { + return param +} + +// Parses assignment pattern around given atom if possible. + +pp.parseMaybeDefault = function(startPos, startLoc, left) { + left = left || this.parseBindingAtom() + if (this.options.ecmaVersion < 6 || !this.eat(tt.eq)) return left + let node = this.startNodeAt(startPos, startLoc) + node.left = left + node.right = this.parseMaybeAssign() + return this.finishNode(node, "AssignmentPattern") +} + +// Verify that a node is an lval — something that can be assigned +// to. +// bindingType can be either: +// 'var' indicating that the lval creates a 'var' binding +// 'let' indicating that the lval creates a lexical ('let' or 'const') binding +// 'none' indicating that the binding should be checked for illegal identifiers, but not for duplicate references + +pp.checkLVal = function(expr, bindingType, checkClashes) { + switch (expr.type) { + case "Identifier": + if (this.strict && this.reservedWordsStrictBind.test(expr.name)) + this.raiseRecoverable(expr.start, (bindingType ? "Binding " : "Assigning to ") + expr.name + " in strict mode") + if (checkClashes) { + if (has(checkClashes, expr.name)) + this.raiseRecoverable(expr.start, "Argument name clash") + checkClashes[expr.name] = true + } + if (bindingType && bindingType !== "none") { + if ( + bindingType === "var" && !this.canDeclareVarName(expr.name) || + bindingType !== "var" && !this.canDeclareLexicalName(expr.name) + ) { + this.raiseRecoverable(expr.start, `Identifier '${expr.name}' has already been declared`) + } + if (bindingType === "var") { + this.declareVarName(expr.name) + } else { + this.declareLexicalName(expr.name) + } + } + break + + case "MemberExpression": + if (bindingType) this.raiseRecoverable(expr.start, "Binding member expression") + break + + case "ObjectPattern": + for (let prop of expr.properties) + this.checkLVal(prop, bindingType, checkClashes) + break + + case "Property": + // AssignmentProperty has type == "Property" + this.checkLVal(expr.value, bindingType, checkClashes) + break + + case "ArrayPattern": + for (let elem of expr.elements) { + if (elem) this.checkLVal(elem, bindingType, checkClashes) + } + break + + case "AssignmentPattern": + this.checkLVal(expr.left, bindingType, checkClashes) + break + + case "RestElement": + this.checkLVal(expr.argument, bindingType, checkClashes) + break + + case "ParenthesizedExpression": + this.checkLVal(expr.expression, bindingType, checkClashes) + break + + default: + this.raise(expr.start, (bindingType ? "Binding" : "Assigning to") + " rvalue") + } +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/node.js b/tools/tests/apps/modules/imports/links/acorn/src/node.js new file mode 100644 index 0000000000..76b5b09825 --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/node.js @@ -0,0 +1,50 @@ +import {Parser} from "./state" +import {SourceLocation} from "./locutil" + +export class Node { + constructor(parser, pos, loc) { + this.type = "" + this.start = pos + this.end = 0 + if (parser.options.locations) + this.loc = new SourceLocation(parser, loc) + if (parser.options.directSourceFile) + this.sourceFile = parser.options.directSourceFile + if (parser.options.ranges) + this.range = [pos, 0] + } +} + +// Start an AST node, attaching a start offset. + +const pp = Parser.prototype + +pp.startNode = function() { + return new Node(this, this.start, this.startLoc) +} + +pp.startNodeAt = function(pos, loc) { + return new Node(this, pos, loc) +} + +// Finish an AST node, adding `type` and `end` properties. + +function finishNodeAt(node, type, pos, loc) { + node.type = type + node.end = pos + if (this.options.locations) + node.loc.end = loc + if (this.options.ranges) + node.range[1] = pos + return node +} + +pp.finishNode = function(node, type) { + return finishNodeAt.call(this, node, type, this.lastTokEnd, this.lastTokEndLoc) +} + +// Finish node at given position + +pp.finishNodeAt = function(node, type, pos, loc) { + return finishNodeAt.call(this, node, type, pos, loc) +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/options.js b/tools/tests/apps/modules/imports/links/acorn/src/options.js new file mode 100644 index 0000000000..9dda44ae88 --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/options.js @@ -0,0 +1,127 @@ +import {has, isArray} from "./util" +import {SourceLocation} from "./locutil" + +// A second optional argument can be given to further configure +// the parser process. These options are recognized: + +export const defaultOptions = { + // `ecmaVersion` indicates the ECMAScript version to parse. Must + // be either 3, 5, 6 (2015), 7 (2016), or 8 (2017). This influences support + // for strict mode, the set of reserved words, and support for + // new syntax features. The default is 7. + ecmaVersion: 7, + // `sourceType` indicates the mode the code should be parsed in. + // Can be either `"script"` or `"module"`. This influences global + // strict mode and parsing of `import` and `export` declarations. + sourceType: "script", + // `onInsertedSemicolon` can be a callback that will be called + // when a semicolon is automatically inserted. It will be passed + // th position of the comma as an offset, and if `locations` is + // enabled, it is given the location as a `{line, column}` object + // as second argument. + onInsertedSemicolon: null, + // `onTrailingComma` is similar to `onInsertedSemicolon`, but for + // trailing commas. + onTrailingComma: null, + // By default, reserved words are only enforced if ecmaVersion >= 5. + // Set `allowReserved` to a boolean value to explicitly turn this on + // an off. When this option has the value "never", reserved words + // and keywords can also not be used as property names. + allowReserved: null, + // When enabled, a return at the top level is not considered an + // error. + allowReturnOutsideFunction: false, + // When enabled, import/export statements are not constrained to + // appearing at the top of the program. + allowImportExportEverywhere: false, + // When enabled, hashbang directive in the beginning of file + // is allowed and treated as a line comment. + allowHashBang: false, + // When `locations` is on, `loc` properties holding objects with + // `start` and `end` properties in `{line, column}` form (with + // line being 1-based and column 0-based) will be attached to the + // nodes. + locations: false, + // A function can be passed as `onToken` option, which will + // cause Acorn to call that function with object in the same + // format as tokens returned from `tokenizer().getToken()`. Note + // that you are not allowed to call the parser from the + // callback—that will corrupt its internal state. + onToken: null, + // A function can be passed as `onComment` option, which will + // cause Acorn to call that function with `(block, text, start, + // end)` parameters whenever a comment is skipped. `block` is a + // boolean indicating whether this is a block (`/* */`) comment, + // `text` is the content of the comment, and `start` and `end` are + // character offsets that denote the start and end of the comment. + // When the `locations` option is on, two more parameters are + // passed, the full `{line, column}` locations of the start and + // end of the comments. Note that you are not allowed to call the + // parser from the callback—that will corrupt its internal state. + onComment: null, + // Nodes have their start and end characters offsets recorded in + // `start` and `end` properties (directly on the node, rather than + // the `loc` object, which holds line/column data. To also add a + // [semi-standardized][range] `range` property holding a `[start, + // end]` array with the same numbers, set the `ranges` option to + // `true`. + // + // [range]: https://bugzilla.mozilla.org/show_bug.cgi?id=745678 + ranges: false, + // It is possible to parse multiple files into a single AST by + // passing the tree produced by parsing the first file as + // `program` option in subsequent parses. This will add the + // toplevel forms of the parsed file to the `Program` (top) node + // of an existing parse tree. + program: null, + // When `locations` is on, you can pass this to record the source + // file in every node's `loc` object. + sourceFile: null, + // This value, if given, is stored in every node, whether + // `locations` is on or off. + directSourceFile: null, + // When enabled, parenthesized expressions are represented by + // (non-standard) ParenthesizedExpression nodes + preserveParens: false, + plugins: {} +} + +// Interpret and default an options object + +export function getOptions(opts) { + let options = {} + + for (let opt in defaultOptions) + options[opt] = opts && has(opts, opt) ? opts[opt] : defaultOptions[opt] + + if (options.ecmaVersion >= 2015) + options.ecmaVersion -= 2009 + + if (options.allowReserved == null) + options.allowReserved = options.ecmaVersion < 5 + + if (isArray(options.onToken)) { + let tokens = options.onToken + options.onToken = (token) => tokens.push(token) + } + if (isArray(options.onComment)) + options.onComment = pushComment(options, options.onComment) + + return options +} + +function pushComment(options, array) { + return function(block, text, start, end, startLoc, endLoc) { + let comment = { + type: block ? "Block" : "Line", + value: text, + start: start, + end: end + } + if (options.locations) + comment.loc = new SourceLocation(this, startLoc, endLoc) + if (options.ranges) + comment.range = [start, end] + array.push(comment) + } +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/parseutil.js b/tools/tests/apps/modules/imports/links/acorn/src/parseutil.js new file mode 100644 index 0000000000..e1736372e5 --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/parseutil.js @@ -0,0 +1,137 @@ +import {types as tt} from "./tokentype" +import {Parser} from "./state" +import {lineBreak, skipWhiteSpace} from "./whitespace" + +const pp = Parser.prototype + +// ## Parser utilities + +const literal = /^(?:'((?:\\.|[^'])*?)'|"((?:\\.|[^"])*?)"|;)/ +pp.strictDirective = function(start) { + for (;;) { + skipWhiteSpace.lastIndex = start + start += skipWhiteSpace.exec(this.input)[0].length + let match = literal.exec(this.input.slice(start)) + if (!match) return false + if ((match[1] || match[2]) == "use strict") return true + start += match[0].length + } +} + +// Predicate that tests whether the next token is of the given +// type, and if yes, consumes it as a side effect. + +pp.eat = function(type) { + if (this.type === type) { + this.next() + return true + } else { + return false + } +} + +// Tests whether parsed token is a contextual keyword. + +pp.isContextual = function(name) { + return this.type === tt.name && this.value === name && !this.containsEsc +} + +// Consumes contextual keyword if possible. + +pp.eatContextual = function(name) { + if (!this.isContextual(name)) return false + this.next() + return true +} + +// Asserts that following token is given contextual keyword. + +pp.expectContextual = function(name) { + if (!this.eatContextual(name)) this.unexpected() +} + +// Test whether a semicolon can be inserted at the current position. + +pp.canInsertSemicolon = function() { + return this.type === tt.eof || + this.type === tt.braceR || + lineBreak.test(this.input.slice(this.lastTokEnd, this.start)) +} + +pp.insertSemicolon = function() { + if (this.canInsertSemicolon()) { + if (this.options.onInsertedSemicolon) + this.options.onInsertedSemicolon(this.lastTokEnd, this.lastTokEndLoc) + return true + } +} + +// Consume a semicolon, or, failing that, see if we are allowed to +// pretend that there is a semicolon at this position. + +pp.semicolon = function() { + if (!this.eat(tt.semi) && !this.insertSemicolon()) this.unexpected() +} + +pp.afterTrailingComma = function(tokType, notNext) { + if (this.type == tokType) { + if (this.options.onTrailingComma) + this.options.onTrailingComma(this.lastTokStart, this.lastTokStartLoc) + if (!notNext) + this.next() + return true + } +} + +// Expect a token of a given type. If found, consume it, otherwise, +// raise an unexpected token error. + +pp.expect = function(type) { + this.eat(type) || this.unexpected() +} + +// Raise an unexpected token error. + +pp.unexpected = function(pos) { + this.raise(pos != null ? pos : this.start, "Unexpected token") +} + +export function DestructuringErrors() { + this.shorthandAssign = + this.trailingComma = + this.parenthesizedAssign = + this.parenthesizedBind = + this.doubleProto = + -1 +} + +pp.checkPatternErrors = function(refDestructuringErrors, isAssign) { + if (!refDestructuringErrors) return + if (refDestructuringErrors.trailingComma > -1) + this.raiseRecoverable(refDestructuringErrors.trailingComma, "Comma is not permitted after the rest element") + let parens = isAssign ? refDestructuringErrors.parenthesizedAssign : refDestructuringErrors.parenthesizedBind + if (parens > -1) this.raiseRecoverable(parens, "Parenthesized pattern") +} + +pp.checkExpressionErrors = function(refDestructuringErrors, andThrow) { + if (!refDestructuringErrors) return false + let {shorthandAssign, doubleProto} = refDestructuringErrors + if (!andThrow) return shorthandAssign >= 0 || doubleProto >= 0 + if (shorthandAssign >= 0) + this.raise(shorthandAssign, "Shorthand property assignments are valid only in destructuring patterns") + if (doubleProto >= 0) + this.raiseRecoverable(doubleProto, "Redefinition of __proto__ property") +} + +pp.checkYieldAwaitInDefaultParams = function() { + if (this.yieldPos && (!this.awaitPos || this.yieldPos < this.awaitPos)) + this.raise(this.yieldPos, "Yield expression cannot be a default value") + if (this.awaitPos) + this.raise(this.awaitPos, "Await expression cannot be a default value") +} + +pp.isSimpleAssignTarget = function(expr) { + if (expr.type === "ParenthesizedExpression") + return this.isSimpleAssignTarget(expr.expression) + return expr.type === "Identifier" || expr.type === "MemberExpression" +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/regexp.js b/tools/tests/apps/modules/imports/links/acorn/src/regexp.js new file mode 100644 index 0000000000..c5953e3012 --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/regexp.js @@ -0,0 +1,1042 @@ +import {isIdentifierStart, isIdentifierChar} from "./identifier.js" +import {Parser} from "./state.js" +import UNICODE_PROPERTY_VALUES from "./unicode-property-data.js" + +const pp = Parser.prototype + +export class RegExpValidationState { + constructor(parser) { + this.parser = parser + this.validFlags = `gim${parser.options.ecmaVersion >= 6 ? "uy" : ""}${parser.options.ecmaVersion >= 9 ? "s" : ""}` + this.source = "" + this.flags = "" + this.start = 0 + this.switchU = false + this.switchN = false + this.pos = 0 + this.lastIntValue = 0 + this.lastStringValue = "" + this.lastAssertionIsQuantifiable = false + this.numCapturingParens = 0 + this.maxBackReference = 0 + this.groupNames = [] + this.backReferenceNames = [] + } + + reset(start, pattern, flags) { + const unicode = flags.indexOf("u") !== -1 + this.start = start | 0 + this.source = pattern + "" + this.flags = flags + this.switchU = unicode && this.parser.options.ecmaVersion >= 6 + this.switchN = unicode && this.parser.options.ecmaVersion >= 9 + } + + raise(message) { + this.parser.raiseRecoverable(this.start, `Invalid regular expression: /${this.source}/: ${message}`) + } + + // If u flag is given, this returns the code point at the index (it combines a surrogate pair). + // Otherwise, this returns the code unit of the index (can be a part of a surrogate pair). + at(i) { + const s = this.source + const l = s.length + if (i >= l) { + return -1 + } + const c = s.charCodeAt(i) + if (!this.switchU || c <= 0xD7FF || c >= 0xE000 || i + 1 >= l) { + return c + } + return (c << 10) + s.charCodeAt(i + 1) - 0x35FDC00 + } + + nextIndex(i) { + const s = this.source + const l = s.length + if (i >= l) { + return l + } + const c = s.charCodeAt(i) + if (!this.switchU || c <= 0xD7FF || c >= 0xE000 || i + 1 >= l) { + return i + 1 + } + return i + 2 + } + + current() { + return this.at(this.pos) + } + + lookahead() { + return this.at(this.nextIndex(this.pos)) + } + + advance() { + this.pos = this.nextIndex(this.pos) + } + + eat(ch) { + if (this.current() === ch) { + this.advance() + return true + } + return false + } +} + +function codePointToString(ch) { + if (ch <= 0xFFFF) return String.fromCharCode(ch) + ch -= 0x10000 + return String.fromCharCode((ch >> 10) + 0xD800, (ch & 0x03FF) + 0xDC00) +} + +/** + * Validate the flags part of a given RegExpLiteral. + * + * @param {RegExpValidationState} state The state to validate RegExp. + * @returns {void} + */ +pp.validateRegExpFlags = function(state) { + const validFlags = state.validFlags + const flags = state.flags + + for (let i = 0; i < flags.length; i++) { + const flag = flags.charAt(i) + if (validFlags.indexOf(flag) == -1) { + this.raise(state.start, "Invalid regular expression flag") + } + if (flags.indexOf(flag, i + 1) > -1) { + this.raise(state.start, "Duplicate regular expression flag") + } + } +} + +/** + * Validate the pattern part of a given RegExpLiteral. + * + * @param {RegExpValidationState} state The state to validate RegExp. + * @returns {void} + */ +pp.validateRegExpPattern = function(state) { + this.regexp_pattern(state) + + // The goal symbol for the parse is |Pattern[~U, ~N]|. If the result of + // parsing contains a |GroupName|, reparse with the goal symbol + // |Pattern[~U, +N]| and use this result instead. Throw a *SyntaxError* + // exception if _P_ did not conform to the grammar, if any elements of _P_ + // were not matched by the parse, or if any Early Error conditions exist. + if (!state.switchN && this.options.ecmaVersion >= 9 && state.groupNames.length > 0) { + state.switchN = true + this.regexp_pattern(state) + } +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-Pattern +pp.regexp_pattern = function(state) { + state.pos = 0 + state.lastIntValue = 0 + state.lastStringValue = "" + state.lastAssertionIsQuantifiable = false + state.numCapturingParens = 0 + state.maxBackReference = 0 + state.groupNames.length = 0 + state.backReferenceNames.length = 0 + + this.regexp_disjunction(state) + + if (state.pos !== state.source.length) { + // Make the same messages as V8. + if (state.eat(0x29 /* ) */)) { + state.raise("Unmatched ')'") + } + if (state.eat(0x5D /* [ */) || state.eat(0x7D /* } */)) { + state.raise("Lone quantifier brackets") + } + } + if (state.maxBackReference > state.numCapturingParens) { + state.raise("Invalid escape") + } + for (const name of state.backReferenceNames) { + if (state.groupNames.indexOf(name) === -1) { + state.raise("Invalid named capture referenced") + } + } +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-Disjunction +pp.regexp_disjunction = function(state) { + this.regexp_alternative(state) + while (state.eat(0x7C /* | */)) { + this.regexp_alternative(state) + } + + // Make the same message as V8. + if (this.regexp_eatQuantifier(state, true)) { + state.raise("Nothing to repeat") + } + if (state.eat(0x7B /* { */)) { + state.raise("Lone quantifier brackets") + } +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-Alternative +pp.regexp_alternative = function(state) { + while (state.pos < state.source.length && this.regexp_eatTerm(state)) + ; +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-Term +pp.regexp_eatTerm = function(state) { + if (this.regexp_eatAssertion(state)) { + // Handle `QuantifiableAssertion Quantifier` alternative. + // `state.lastAssertionIsQuantifiable` is true if the last eaten Assertion + // is a QuantifiableAssertion. + if (state.lastAssertionIsQuantifiable && this.regexp_eatQuantifier(state)) { + // Make the same message as V8. + if (state.switchU) { + state.raise("Invalid quantifier") + } + } + return true + } + + if (state.switchU ? this.regexp_eatAtom(state) : this.regexp_eatExtendedAtom(state)) { + this.regexp_eatQuantifier(state) + return true + } + + return false +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-Assertion +pp.regexp_eatAssertion = function(state) { + const start = state.pos + state.lastAssertionIsQuantifiable = false + + // ^, $ + if (state.eat(0x5E /* ^ */) || state.eat(0x24 /* $ */)) { + return true + } + + // \b \B + if (state.eat(0x5C /* \ */)) { + if (state.eat(0x42 /* B */) || state.eat(0x62 /* b */)) { + return true + } + state.pos = start + } + + // Lookahead / Lookbehind + if (state.eat(0x28 /* ( */) && state.eat(0x3F /* ? */)) { + let lookbehind = false + if (this.options.ecmaVersion >= 9) { + lookbehind = state.eat(0x3C /* < */) + } + if (state.eat(0x3D /* = */) || state.eat(0x21 /* ! */)) { + this.regexp_disjunction(state) + if (!state.eat(0x29 /* ) */)) { + state.raise("Unterminated group") + } + state.lastAssertionIsQuantifiable = !lookbehind + return true + } + } + + state.pos = start + return false +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-Quantifier +pp.regexp_eatQuantifier = function(state, noError = false) { + if (this.regexp_eatQuantifierPrefix(state, noError)) { + state.eat(0x3F /* ? */) + return true + } + return false +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-QuantifierPrefix +pp.regexp_eatQuantifierPrefix = function(state, noError) { + return ( + state.eat(0x2A /* * */) || + state.eat(0x2B /* + */) || + state.eat(0x3F /* ? */) || + this.regexp_eatBracedQuantifier(state, noError) + ) +} +pp.regexp_eatBracedQuantifier = function(state, noError) { + const start = state.pos + if (state.eat(0x7B /* { */)) { + let min = 0, max = -1 + if (this.regexp_eatDecimalDigits(state)) { + min = state.lastIntValue + if (state.eat(0x2C /* , */) && this.regexp_eatDecimalDigits(state)) { + max = state.lastIntValue + } + if (state.eat(0x7D /* } */)) { + // SyntaxError in https://www.ecma-international.org/ecma-262/8.0/#sec-term + if (max !== -1 && max < min && !noError) { + state.raise("numbers out of order in {} quantifier") + } + return true + } + } + if (state.switchU && !noError) { + state.raise("Incomplete quantifier") + } + state.pos = start + } + return false +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-Atom +pp.regexp_eatAtom = function(state) { + return ( + this.regexp_eatPatternCharacters(state) || + state.eat(0x2E /* . */) || + this.regexp_eatReverseSolidusAtomEscape(state) || + this.regexp_eatCharacterClass(state) || + this.regexp_eatUncapturingGroup(state) || + this.regexp_eatCapturingGroup(state) + ) +} +pp.regexp_eatReverseSolidusAtomEscape = function(state) { + const start = state.pos + if (state.eat(0x5C /* \ */)) { + if (this.regexp_eatAtomEscape(state)) { + return true + } + state.pos = start + } + return false +} +pp.regexp_eatUncapturingGroup = function(state) { + const start = state.pos + if (state.eat(0x28 /* ( */)) { + if (state.eat(0x3F /* ? */) && state.eat(0x3A /* : */)) { + this.regexp_disjunction(state) + if (state.eat(0x29 /* ) */)) { + return true + } + state.raise("Unterminated group") + } + state.pos = start + } + return false +} +pp.regexp_eatCapturingGroup = function(state) { + if (state.eat(0x28 /* ( */)) { + if (this.options.ecmaVersion >= 9) { + this.regexp_groupSpecifier(state) + } else if (state.current() === 0x3F /* ? */) { + state.raise("Invalid group") + } + this.regexp_disjunction(state) + if (state.eat(0x29 /* ) */)) { + state.numCapturingParens += 1 + return true + } + state.raise("Unterminated group") + } + return false +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-ExtendedAtom +pp.regexp_eatExtendedAtom = function(state) { + return ( + state.eat(0x2E /* . */) || + this.regexp_eatReverseSolidusAtomEscape(state) || + this.regexp_eatCharacterClass(state) || + this.regexp_eatUncapturingGroup(state) || + this.regexp_eatCapturingGroup(state) || + this.regexp_eatInvalidBracedQuantifier(state) || + this.regexp_eatExtendedPatternCharacter(state) + ) +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-InvalidBracedQuantifier +pp.regexp_eatInvalidBracedQuantifier = function(state) { + if (this.regexp_eatBracedQuantifier(state, true)) { + state.raise("Nothing to repeat") + } + return false +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-SyntaxCharacter +pp.regexp_eatSyntaxCharacter = function(state) { + const ch = state.current() + if (isSyntaxCharacter(ch)) { + state.lastIntValue = ch + state.advance() + return true + } + return false +} +function isSyntaxCharacter(ch) { + return ( + ch === 0x24 /* $ */ || + ch >= 0x28 /* ( */ && ch <= 0x2B /* + */ || + ch === 0x2E /* . */ || + ch === 0x3F /* ? */ || + ch >= 0x5B /* [ */ && ch <= 0x5E /* ^ */ || + ch >= 0x7B /* { */ && ch <= 0x7D /* } */ + ) +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-PatternCharacter +// But eat eager. +pp.regexp_eatPatternCharacters = function(state) { + const start = state.pos + let ch = 0 + while ((ch = state.current()) !== -1 && !isSyntaxCharacter(ch)) { + state.advance() + } + return state.pos !== start +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-ExtendedPatternCharacter +pp.regexp_eatExtendedPatternCharacter = function(state) { + const ch = state.current() + if ( + ch !== -1 && + ch !== 0x24 /* $ */ && + !(ch >= 0x28 /* ( */ && ch <= 0x2B /* + */) && + ch !== 0x2E /* . */ && + ch !== 0x3F /* ? */ && + ch !== 0x5B /* [ */ && + ch !== 0x5E /* ^ */ && + ch !== 0x7C /* | */ + ) { + state.advance() + return true + } + return false +} + +// GroupSpecifier[U] :: +// [empty] +// `?` GroupName[?U] +pp.regexp_groupSpecifier = function(state) { + if (state.eat(0x3F /* ? */)) { + if (this.regexp_eatGroupName(state)) { + if (state.groupNames.indexOf(state.lastStringValue) !== -1) { + state.raise("Duplicate capture group name") + } + state.groupNames.push(state.lastStringValue) + return + } + state.raise("Invalid group") + } +} + +// GroupName[U] :: +// `<` RegExpIdentifierName[?U] `>` +// Note: this updates `state.lastStringValue` property with the eaten name. +pp.regexp_eatGroupName = function(state) { + state.lastStringValue = "" + if (state.eat(0x3C /* < */)) { + if (this.regexp_eatRegExpIdentifierName(state) && state.eat(0x3E /* > */)) { + return true + } + state.raise("Invalid capture group name") + } + return false +} + +// RegExpIdentifierName[U] :: +// RegExpIdentifierStart[?U] +// RegExpIdentifierName[?U] RegExpIdentifierPart[?U] +// Note: this updates `state.lastStringValue` property with the eaten name. +pp.regexp_eatRegExpIdentifierName = function(state) { + state.lastStringValue = "" + if (this.regexp_eatRegExpIdentifierStart(state)) { + state.lastStringValue += codePointToString(state.lastIntValue) + while (this.regexp_eatRegExpIdentifierPart(state)) { + state.lastStringValue += codePointToString(state.lastIntValue) + } + return true + } + return false +} + +// RegExpIdentifierStart[U] :: +// UnicodeIDStart +// `$` +// `_` +// `\` RegExpUnicodeEscapeSequence[?U] +pp.regexp_eatRegExpIdentifierStart = function(state) { + const start = state.pos + let ch = state.current() + state.advance() + + if (ch === 0x5C /* \ */ && this.regexp_eatRegExpUnicodeEscapeSequence(state)) { + ch = state.lastIntValue + } + if (isRegExpIdentifierStart(ch)) { + state.lastIntValue = ch + return true + } + + state.pos = start + return false +} +function isRegExpIdentifierStart(ch) { + return isIdentifierStart(ch, true) || ch === 0x24 /* $ */ || ch === 0x5F /* _ */ +} + +// RegExpIdentifierPart[U] :: +// UnicodeIDContinue +// `$` +// `_` +// `\` RegExpUnicodeEscapeSequence[?U] +// +// +pp.regexp_eatRegExpIdentifierPart = function(state) { + const start = state.pos + let ch = state.current() + state.advance() + + if (ch === 0x5C /* \ */ && this.regexp_eatRegExpUnicodeEscapeSequence(state)) { + ch = state.lastIntValue + } + if (isRegExpIdentifierPart(ch)) { + state.lastIntValue = ch + return true + } + + state.pos = start + return false +} +function isRegExpIdentifierPart(ch) { + return isIdentifierChar(ch, true) || ch === 0x24 /* $ */ || ch === 0x5F /* _ */ || ch === 0x200C /* */ || ch === 0x200D /* */ +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-AtomEscape +pp.regexp_eatAtomEscape = function(state) { + if ( + this.regexp_eatBackReference(state) || + this.regexp_eatCharacterClassEscape(state) || + this.regexp_eatCharacterEscape(state) || + (state.switchN && this.regexp_eatKGroupName(state)) + ) { + return true + } + if (state.switchU) { + // Make the same message as V8. + if (state.current() === 0x63 /* c */) { + state.raise("Invalid unicode escape") + } + state.raise("Invalid escape") + } + return false +} +pp.regexp_eatBackReference = function(state) { + const start = state.pos + if (this.regexp_eatDecimalEscape(state)) { + const n = state.lastIntValue + if (state.switchU) { + // For SyntaxError in https://www.ecma-international.org/ecma-262/8.0/#sec-atomescape + if (n > state.maxBackReference) { + state.maxBackReference = n + } + return true + } + if (n <= state.numCapturingParens) { + return true + } + state.pos = start + } + return false +} +pp.regexp_eatKGroupName = function(state) { + if (state.eat(0x6B /* k */)) { + if (this.regexp_eatGroupName(state)) { + state.backReferenceNames.push(state.lastStringValue) + return true + } + state.raise("Invalid named reference") + } + return false +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-CharacterEscape +pp.regexp_eatCharacterEscape = function(state) { + return ( + this.regexp_eatControlEscape(state) || + this.regexp_eatCControlLetter(state) || + this.regexp_eatZero(state) || + this.regexp_eatHexEscapeSequence(state) || + this.regexp_eatRegExpUnicodeEscapeSequence(state) || + (!state.switchU && this.regexp_eatLegacyOctalEscapeSequence(state)) || + this.regexp_eatIdentityEscape(state) + ) +} +pp.regexp_eatCControlLetter = function(state) { + const start = state.pos + if (state.eat(0x63 /* c */)) { + if (this.regexp_eatControlLetter(state)) { + return true + } + state.pos = start + } + return false +} +pp.regexp_eatZero = function(state) { + if (state.current() === 0x30 /* 0 */ && !isDecimalDigit(state.lookahead())) { + state.lastIntValue = 0 + state.advance() + return true + } + return false +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-ControlEscape +pp.regexp_eatControlEscape = function(state) { + const ch = state.current() + if (ch === 0x74 /* t */) { + state.lastIntValue = 0x09 /* \t */ + state.advance() + return true + } + if (ch === 0x6E /* n */) { + state.lastIntValue = 0x0A /* \n */ + state.advance() + return true + } + if (ch === 0x76 /* v */) { + state.lastIntValue = 0x0B /* \v */ + state.advance() + return true + } + if (ch === 0x66 /* f */) { + state.lastIntValue = 0x0C /* \f */ + state.advance() + return true + } + if (ch === 0x72 /* r */) { + state.lastIntValue = 0x0D /* \r */ + state.advance() + return true + } + return false +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-ControlLetter +pp.regexp_eatControlLetter = function(state) { + const ch = state.current() + if (isControlLetter(ch)) { + state.lastIntValue = ch % 0x20 + state.advance() + return true + } + return false +} +function isControlLetter(ch) { + return ( + (ch >= 0x41 /* A */ && ch <= 0x5A /* Z */) || + (ch >= 0x61 /* a */ && ch <= 0x7A /* z */) + ) +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-RegExpUnicodeEscapeSequence +pp.regexp_eatRegExpUnicodeEscapeSequence = function(state) { + const start = state.pos + + if (state.eat(0x75 /* u */)) { + if (this.regexp_eatFixedHexDigits(state, 4)) { + const lead = state.lastIntValue + if (state.switchU && lead >= 0xD800 && lead <= 0xDBFF) { + const leadSurrogateEnd = state.pos + if (state.eat(0x5C /* \ */) && state.eat(0x75 /* u */) && this.regexp_eatFixedHexDigits(state, 4)) { + const trail = state.lastIntValue + if (trail >= 0xDC00 && trail <= 0xDFFF) { + state.lastIntValue = (lead - 0xD800) * 0x400 + (trail - 0xDC00) + 0x10000 + return true + } + } + state.pos = leadSurrogateEnd + state.lastIntValue = lead + } + return true + } + if ( + state.switchU && + state.eat(0x7B /* { */) && + this.regexp_eatHexDigits(state) && + state.eat(0x7D /* } */) && + isValidUnicode(state.lastIntValue) + ) { + return true + } + if (state.switchU) { + state.raise("Invalid unicode escape") + } + state.pos = start + } + + return false +} +function isValidUnicode(ch) { + return ch >= 0 && ch <= 0x10FFFF +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-IdentityEscape +pp.regexp_eatIdentityEscape = function(state) { + if (state.switchU) { + if (this.regexp_eatSyntaxCharacter(state)) { + return true + } + if (state.eat(0x2F /* / */)) { + state.lastIntValue = 0x2F /* / */ + return true + } + return false + } + + const ch = state.current() + if (ch !== 0x63 /* c */ && (!state.switchN || ch !== 0x6B /* k */)) { + state.lastIntValue = ch + state.advance() + return true + } + + return false +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-DecimalEscape +pp.regexp_eatDecimalEscape = function(state) { + state.lastIntValue = 0 + let ch = state.current() + if (ch >= 0x31 /* 1 */ && ch <= 0x39 /* 9 */) { + do { + state.lastIntValue = 10 * state.lastIntValue + (ch - 0x30 /* 0 */) + state.advance() + } while ((ch = state.current()) >= 0x30 /* 0 */ && ch <= 0x39 /* 9 */) + return true + } + return false +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-CharacterClassEscape +pp.regexp_eatCharacterClassEscape = function(state) { + const ch = state.current() + + if (isCharacterClassEscape(ch)) { + state.lastIntValue = -1 + state.advance() + return true + } + + if ( + state.switchU && + this.options.ecmaVersion >= 9 && + (ch === 0x50 /* P */ || ch === 0x70 /* p */) + ) { + state.lastIntValue = -1 + state.advance() + if ( + state.eat(0x7B /* { */) && + this.regexp_eatUnicodePropertyValueExpression(state) && + state.eat(0x7D /* } */) + ) { + return true + } + state.raise("Invalid property name") + } + + return false +} +function isCharacterClassEscape(ch) { + return ( + ch === 0x64 /* d */ || + ch === 0x44 /* D */ || + ch === 0x73 /* s */ || + ch === 0x53 /* S */ || + ch === 0x77 /* w */ || + ch === 0x57 /* W */ + ) +} + +// UnicodePropertyValueExpression :: +// UnicodePropertyName `=` UnicodePropertyValue +// LoneUnicodePropertyNameOrValue +pp.regexp_eatUnicodePropertyValueExpression = function(state) { + const start = state.pos + + // UnicodePropertyName `=` UnicodePropertyValue + if (this.regexp_eatUnicodePropertyName(state) && state.eat(0x3D /* = */)) { + const name = state.lastStringValue + if (this.regexp_eatUnicodePropertyValue(state)) { + const value = state.lastStringValue + this.regexp_validateUnicodePropertyNameAndValue(state, name, value) + return true + } + } + state.pos = start + + // LoneUnicodePropertyNameOrValue + if (this.regexp_eatLoneUnicodePropertyNameOrValue(state)) { + const nameOrValue = state.lastStringValue + this.regexp_validateUnicodePropertyNameOrValue(state, nameOrValue) + return true + } + return false +} +pp.regexp_validateUnicodePropertyNameAndValue = function(state, name, value) { + if (!UNICODE_PROPERTY_VALUES.hasOwnProperty(name) || UNICODE_PROPERTY_VALUES[name].indexOf(value) === -1) { + state.raise("Invalid property name") + } +} +pp.regexp_validateUnicodePropertyNameOrValue = function(state, nameOrValue) { + if (UNICODE_PROPERTY_VALUES.$LONE.indexOf(nameOrValue) === -1) { + state.raise("Invalid property name") + } +} + +// UnicodePropertyName :: +// UnicodePropertyNameCharacters +pp.regexp_eatUnicodePropertyName = function(state) { + let ch = 0 + state.lastStringValue = "" + while (isUnicodePropertyNameCharacter(ch = state.current())) { + state.lastStringValue += codePointToString(ch) + state.advance() + } + return state.lastStringValue !== "" +} +function isUnicodePropertyNameCharacter(ch) { + return isControlLetter(ch) || ch === 0x5F /* _ */ +} + +// UnicodePropertyValue :: +// UnicodePropertyValueCharacters +pp.regexp_eatUnicodePropertyValue = function(state) { + let ch = 0 + state.lastStringValue = "" + while (isUnicodePropertyValueCharacter(ch = state.current())) { + state.lastStringValue += codePointToString(ch) + state.advance() + } + return state.lastStringValue !== "" +} +function isUnicodePropertyValueCharacter(ch) { + return isUnicodePropertyNameCharacter(ch) || isDecimalDigit(ch) +} + +// LoneUnicodePropertyNameOrValue :: +// UnicodePropertyValueCharacters +pp.regexp_eatLoneUnicodePropertyNameOrValue = function(state) { + return this.regexp_eatUnicodePropertyValue(state) +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-CharacterClass +pp.regexp_eatCharacterClass = function(state) { + if (state.eat(0x5B /* [ */)) { + state.eat(0x5E /* ^ */) + this.regexp_classRanges(state) + if (state.eat(0x5D /* [ */)) { + return true + } + // Unreachable since it threw "unterminated regular expression" error before. + state.raise("Unterminated character class") + } + return false +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-ClassRanges +// https://www.ecma-international.org/ecma-262/8.0/#prod-NonemptyClassRanges +// https://www.ecma-international.org/ecma-262/8.0/#prod-NonemptyClassRangesNoDash +pp.regexp_classRanges = function(state) { + while (this.regexp_eatClassAtom(state)) { + const left = state.lastIntValue + if (state.eat(0x2D /* - */) && this.regexp_eatClassAtom(state)) { + const right = state.lastIntValue + if (state.switchU && (left === -1 || right === -1)) { + state.raise("Invalid character class") + } + if (left !== -1 && right !== -1 && left > right) { + state.raise("Range out of order in character class") + } + } + } +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-ClassAtom +// https://www.ecma-international.org/ecma-262/8.0/#prod-ClassAtomNoDash +pp.regexp_eatClassAtom = function(state) { + const start = state.pos + + if (state.eat(0x5C /* \ */)) { + if (this.regexp_eatClassEscape(state)) { + return true + } + if (state.switchU) { + // Make the same message as V8. + const ch = state.current() + if (ch === 0x63 /* c */ || isOctalDigit(ch)) { + state.raise("Invalid class escape") + } + state.raise("Invalid escape") + } + state.pos = start + } + + const ch = state.current() + if (ch !== 0x5D /* [ */) { + state.lastIntValue = ch + state.advance() + return true + } + + return false +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-ClassEscape +pp.regexp_eatClassEscape = function(state) { + const start = state.pos + + if (state.eat(0x62 /* b */)) { + state.lastIntValue = 0x08 /* */ + return true + } + + if (state.switchU && state.eat(0x2D /* - */)) { + state.lastIntValue = 0x2D /* - */ + return true + } + + if (!state.switchU && state.eat(0x63 /* c */)) { + if (this.regexp_eatClassControlLetter(state)) { + return true + } + state.pos = start + } + + return ( + this.regexp_eatCharacterClassEscape(state) || + this.regexp_eatCharacterEscape(state) + ) +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-ClassControlLetter +pp.regexp_eatClassControlLetter = function(state) { + const ch = state.current() + if (isDecimalDigit(ch) || ch === 0x5F /* _ */) { + state.lastIntValue = ch % 0x20 + state.advance() + return true + } + return false +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-HexEscapeSequence +pp.regexp_eatHexEscapeSequence = function(state) { + const start = state.pos + if (state.eat(0x78 /* x */)) { + if (this.regexp_eatFixedHexDigits(state, 2)) { + return true + } + if (state.switchU) { + state.raise("Invalid escape") + } + state.pos = start + } + return false +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-DecimalDigits +pp.regexp_eatDecimalDigits = function(state) { + const start = state.pos + let ch = 0 + state.lastIntValue = 0 + while (isDecimalDigit(ch = state.current())) { + state.lastIntValue = 10 * state.lastIntValue + (ch - 0x30 /* 0 */) + state.advance() + } + return state.pos !== start +} +function isDecimalDigit(ch) { + return ch >= 0x30 /* 0 */ && ch <= 0x39 /* 9 */ +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-HexDigits +pp.regexp_eatHexDigits = function(state) { + const start = state.pos + let ch = 0 + state.lastIntValue = 0 + while (isHexDigit(ch = state.current())) { + state.lastIntValue = 16 * state.lastIntValue + hexToInt(ch) + state.advance() + } + return state.pos !== start +} +function isHexDigit(ch) { + return ( + (ch >= 0x30 /* 0 */ && ch <= 0x39 /* 9 */) || + (ch >= 0x41 /* A */ && ch <= 0x46 /* F */) || + (ch >= 0x61 /* a */ && ch <= 0x66 /* f */) + ) +} +function hexToInt(ch) { + if (ch >= 0x41 /* A */ && ch <= 0x46 /* F */) { + return 10 + (ch - 0x41 /* A */) + } + if (ch >= 0x61 /* a */ && ch <= 0x66 /* f */) { + return 10 + (ch - 0x61 /* a */) + } + return ch - 0x30 /* 0 */ +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-annexB-LegacyOctalEscapeSequence +// Allows only 0-377(octal) i.e. 0-255(decimal). +pp.regexp_eatLegacyOctalEscapeSequence = function(state) { + if (this.regexp_eatOctalDigit(state)) { + const n1 = state.lastIntValue + if (this.regexp_eatOctalDigit(state)) { + const n2 = state.lastIntValue + if (n1 <= 3 && this.regexp_eatOctalDigit(state)) { + state.lastIntValue = n1 * 64 + n2 * 8 + state.lastIntValue + } else { + state.lastIntValue = n1 * 8 + n2 + } + } else { + state.lastIntValue = n1 + } + return true + } + return false +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-OctalDigit +pp.regexp_eatOctalDigit = function(state) { + const ch = state.current() + if (isOctalDigit(ch)) { + state.lastIntValue = ch - 0x30 /* 0 */ + state.advance() + return true + } + state.lastIntValue = 0 + return false +} +function isOctalDigit(ch) { + return ch >= 0x30 /* 0 */ && ch <= 0x37 /* 7 */ +} + +// https://www.ecma-international.org/ecma-262/8.0/#prod-Hex4Digits +// https://www.ecma-international.org/ecma-262/8.0/#prod-HexDigit +// And HexDigit HexDigit in https://www.ecma-international.org/ecma-262/8.0/#prod-HexEscapeSequence +pp.regexp_eatFixedHexDigits = function(state, length) { + const start = state.pos + state.lastIntValue = 0 + for (let i = 0; i < length; ++i) { + const ch = state.current() + if (!isHexDigit(ch)) { + state.pos = start + return false + } + state.lastIntValue = 16 * state.lastIntValue + hexToInt(ch) + state.advance() + } + return true +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/scope.js b/tools/tests/apps/modules/imports/links/acorn/src/scope.js new file mode 100644 index 0000000000..eab0002c04 --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/scope.js @@ -0,0 +1,74 @@ +import {Parser} from "./state" +import {has} from "./util" + +const pp = Parser.prototype + +// Object.assign polyfill +const assign = Object.assign || function(target, ...sources) { + for (let source of sources) { + for (const key in source) { + if (has(source, key)) { + target[key] = source[key] + } + } + } + return target +} + +// The functions in this module keep track of declared variables in the current scope in order to detect duplicate variable names. + +pp.enterFunctionScope = function() { + // var: a hash of var-declared names in the current lexical scope + // lexical: a hash of lexically-declared names in the current lexical scope + // childVar: a hash of var-declared names in all child lexical scopes of the current lexical scope (within the current function scope) + // parentLexical: a hash of lexically-declared names in all parent lexical scopes of the current lexical scope (within the current function scope) + this.scopeStack.push({var: {}, lexical: {}, childVar: {}, parentLexical: {}}) +} + +pp.exitFunctionScope = function() { + this.scopeStack.pop() +} + +pp.enterLexicalScope = function() { + const parentScope = this.scopeStack[this.scopeStack.length - 1] + const childScope = {var: {}, lexical: {}, childVar: {}, parentLexical: {}} + + this.scopeStack.push(childScope) + assign(childScope.parentLexical, parentScope.lexical, parentScope.parentLexical) +} + +pp.exitLexicalScope = function() { + const childScope = this.scopeStack.pop() + const parentScope = this.scopeStack[this.scopeStack.length - 1] + + assign(parentScope.childVar, childScope.var, childScope.childVar) +} + +/** + * A name can be declared with `var` if there are no variables with the same name declared with `let`/`const` + * in the current lexical scope or any of the parent lexical scopes in this function. + */ +pp.canDeclareVarName = function(name) { + const currentScope = this.scopeStack[this.scopeStack.length - 1] + + return !has(currentScope.lexical, name) && !has(currentScope.parentLexical, name) +} + +/** + * A name can be declared with `let`/`const` if there are no variables with the same name declared with `let`/`const` + * in the current scope, and there are no variables with the same name declared with `var` in the current scope or in + * any child lexical scopes in this function. + */ +pp.canDeclareLexicalName = function(name) { + const currentScope = this.scopeStack[this.scopeStack.length - 1] + + return !has(currentScope.lexical, name) && !has(currentScope.var, name) && !has(currentScope.childVar, name) +} + +pp.declareVarName = function(name) { + this.scopeStack[this.scopeStack.length - 1].var[name] = true +} + +pp.declareLexicalName = function(name) { + this.scopeStack[this.scopeStack.length - 1].lexical[name] = true +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/state.js b/tools/tests/apps/modules/imports/links/acorn/src/state.js new file mode 100644 index 0000000000..185fe28648 --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/state.js @@ -0,0 +1,118 @@ +import {reservedWords, keywords} from "./identifier" +import {types as tt} from "./tokentype" +import {lineBreak} from "./whitespace" +import {getOptions} from "./options" + +// Registered plugins +export const plugins = {} + +function keywordRegexp(words) { + return new RegExp("^(?:" + words.replace(/ /g, "|") + ")$") +} + +export class Parser { + constructor(options, input, startPos) { + this.options = options = getOptions(options) + this.sourceFile = options.sourceFile + this.keywords = keywordRegexp(keywords[options.ecmaVersion >= 6 ? 6 : 5]) + let reserved = "" + if (!options.allowReserved) { + for (let v = options.ecmaVersion;; v--) + if (reserved = reservedWords[v]) break + if (options.sourceType == "module") reserved += " await" + } + this.reservedWords = keywordRegexp(reserved) + let reservedStrict = (reserved ? reserved + " " : "") + reservedWords.strict + this.reservedWordsStrict = keywordRegexp(reservedStrict) + this.reservedWordsStrictBind = keywordRegexp(reservedStrict + " " + reservedWords.strictBind) + this.input = String(input) + + // Used to signal to callers of `readWord1` whether the word + // contained any escape sequences. This is needed because words with + // escape sequences must not be interpreted as keywords. + this.containsEsc = false + + // Load plugins + this.loadPlugins(options.plugins) + + // Set up token state + + // The current position of the tokenizer in the input. + if (startPos) { + this.pos = startPos + this.lineStart = this.input.lastIndexOf("\n", startPos - 1) + 1 + this.curLine = this.input.slice(0, this.lineStart).split(lineBreak).length + } else { + this.pos = this.lineStart = 0 + this.curLine = 1 + } + + // Properties of the current token: + // Its type + this.type = tt.eof + // For tokens that include more information than their type, the value + this.value = null + // Its start and end offset + this.start = this.end = this.pos + // And, if locations are used, the {line, column} object + // corresponding to those offsets + this.startLoc = this.endLoc = this.curPosition() + + // Position information for the previous token + this.lastTokEndLoc = this.lastTokStartLoc = null + this.lastTokStart = this.lastTokEnd = this.pos + + // The context stack is used to superficially track syntactic + // context to predict whether a regular expression is allowed in a + // given position. + this.context = this.initialContext() + this.exprAllowed = true + + // Figure out if it's a module code. + this.inModule = options.sourceType === "module" + this.strict = this.inModule || this.strictDirective(this.pos) + + // Used to signify the start of a potential arrow function + this.potentialArrowAt = -1 + + // Flags to track whether we are in a function, a generator, an async function. + this.inFunction = this.inGenerator = this.inAsync = false + // Positions to delayed-check that yield/await does not exist in default parameters. + this.yieldPos = this.awaitPos = 0 + // Labels in scope. + this.labels = [] + + // If enabled, skip leading hashbang line. + if (this.pos === 0 && options.allowHashBang && this.input.slice(0, 2) === "#!") + this.skipLineComment(2) + + // Scope tracking for duplicate variable names (see scope.js) + this.scopeStack = [] + this.enterFunctionScope() + + // For RegExp validation + this.regexpState = null + } + + // DEPRECATED Kept for backwards compatibility until 3.0 in case a plugin uses them + isKeyword(word) { return this.keywords.test(word) } + isReservedWord(word) { return this.reservedWords.test(word) } + + extend(name, f) { + this[name] = f(this[name]) + } + + loadPlugins(pluginConfigs) { + for (let name in pluginConfigs) { + let plugin = plugins[name] + if (!plugin) throw new Error("Plugin '" + name + "' not found") + plugin(this, pluginConfigs[name]) + } + } + + parse() { + let node = this.options.program || this.startNode() + this.nextToken() + return this.parseTopLevel(node) + } +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/statement.js b/tools/tests/apps/modules/imports/links/acorn/src/statement.js new file mode 100644 index 0000000000..2612125df3 --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/statement.js @@ -0,0 +1,810 @@ +import {types as tt} from "./tokentype" +import {Parser} from "./state" +import {lineBreak, skipWhiteSpace} from "./whitespace" +import {isIdentifierStart, isIdentifierChar, keywordRelationalOperator} from "./identifier" +import {has} from "./util" +import {DestructuringErrors} from "./parseutil" + +const pp = Parser.prototype + +// ### Statement parsing + +// Parse a program. Initializes the parser, reads any number of +// statements, and wraps them in a Program node. Optionally takes a +// `program` argument. If present, the statements will be appended +// to its body instead of creating a new node. + +pp.parseTopLevel = function(node) { + let exports = {} + if (!node.body) node.body = [] + while (this.type !== tt.eof) { + let stmt = this.parseStatement(true, true, exports) + node.body.push(stmt) + } + this.adaptDirectivePrologue(node.body) + this.next() + if (this.options.ecmaVersion >= 6) { + node.sourceType = this.options.sourceType + } + return this.finishNode(node, "Program") +} + +const loopLabel = {kind: "loop"}, switchLabel = {kind: "switch"} + +pp.isLet = function() { + if (this.options.ecmaVersion < 6 || !this.isContextual("let")) return false + skipWhiteSpace.lastIndex = this.pos + let skip = skipWhiteSpace.exec(this.input) + let next = this.pos + skip[0].length, nextCh = this.input.charCodeAt(next) + if (nextCh === 91 || nextCh == 123) return true // '{' and '[' + if (isIdentifierStart(nextCh, true)) { + let pos = next + 1 + while (isIdentifierChar(this.input.charCodeAt(pos), true)) ++pos + let ident = this.input.slice(next, pos) + if (!keywordRelationalOperator.test(ident)) return true + } + return false +} + +// check 'async [no LineTerminator here] function' +// - 'async /*foo*/ function' is OK. +// - 'async /*\n*/ function' is invalid. +pp.isAsyncFunction = function() { + if (this.options.ecmaVersion < 8 || !this.isContextual("async")) + return false + + skipWhiteSpace.lastIndex = this.pos + let skip = skipWhiteSpace.exec(this.input) + let next = this.pos + skip[0].length + return !lineBreak.test(this.input.slice(this.pos, next)) && + this.input.slice(next, next + 8) === "function" && + (next + 8 == this.input.length || !isIdentifierChar(this.input.charAt(next + 8))) +} + +// Parse a single statement. +// +// If expecting a statement and finding a slash operator, parse a +// regular expression literal. This is to handle cases like +// `if (foo) /blah/.exec(foo)`, where looking at the previous token +// does not help. + +pp.parseStatement = function(declaration, topLevel, exports) { + let starttype = this.type, node = this.startNode(), kind + + if (this.isLet()) { + starttype = tt._var + kind = "let" + } + + // Most types of statements are recognized by the keyword they + // start with. Many are trivial to parse, some require a bit of + // complexity. + + switch (starttype) { + case tt._break: case tt._continue: return this.parseBreakContinueStatement(node, starttype.keyword) + case tt._debugger: return this.parseDebuggerStatement(node) + case tt._do: return this.parseDoStatement(node) + case tt._for: return this.parseForStatement(node) + case tt._function: + if (!declaration && this.options.ecmaVersion >= 6) this.unexpected() + return this.parseFunctionStatement(node, false) + case tt._class: + if (!declaration) this.unexpected() + return this.parseClass(node, true) + case tt._if: return this.parseIfStatement(node) + case tt._return: return this.parseReturnStatement(node) + case tt._switch: return this.parseSwitchStatement(node) + case tt._throw: return this.parseThrowStatement(node) + case tt._try: return this.parseTryStatement(node) + case tt._const: case tt._var: + kind = kind || this.value + if (!declaration && kind != "var") this.unexpected() + return this.parseVarStatement(node, kind) + case tt._while: return this.parseWhileStatement(node) + case tt._with: return this.parseWithStatement(node) + case tt.braceL: return this.parseBlock() + case tt.semi: return this.parseEmptyStatement(node) + case tt._export: + case tt._import: + if (!this.options.allowImportExportEverywhere) { + if (!topLevel) + this.raise(this.start, "'import' and 'export' may only appear at the top level") + if (!this.inModule) + this.raise(this.start, "'import' and 'export' may appear only with 'sourceType: module'") + } + return starttype === tt._import ? this.parseImport(node) : this.parseExport(node, exports) + + // If the statement does not start with a statement keyword or a + // brace, it's an ExpressionStatement or LabeledStatement. We + // simply start parsing an expression, and afterwards, if the + // next token is a colon and the expression was a simple + // Identifier node, we switch to interpreting it as a label. + default: + if (this.isAsyncFunction()) { + if (!declaration) this.unexpected() + this.next() + return this.parseFunctionStatement(node, true) + } + + let maybeName = this.value, expr = this.parseExpression() + if (starttype === tt.name && expr.type === "Identifier" && this.eat(tt.colon)) + return this.parseLabeledStatement(node, maybeName, expr) + else return this.parseExpressionStatement(node, expr) + } +} + +pp.parseBreakContinueStatement = function(node, keyword) { + let isBreak = keyword == "break" + this.next() + if (this.eat(tt.semi) || this.insertSemicolon()) node.label = null + else if (this.type !== tt.name) this.unexpected() + else { + node.label = this.parseIdent() + this.semicolon() + } + + // Verify that there is an actual destination to break or + // continue to. + let i = 0 + for (; i < this.labels.length; ++i) { + let lab = this.labels[i] + if (node.label == null || lab.name === node.label.name) { + if (lab.kind != null && (isBreak || lab.kind === "loop")) break + if (node.label && isBreak) break + } + } + if (i === this.labels.length) this.raise(node.start, "Unsyntactic " + keyword) + return this.finishNode(node, isBreak ? "BreakStatement" : "ContinueStatement") +} + +pp.parseDebuggerStatement = function(node) { + this.next() + this.semicolon() + return this.finishNode(node, "DebuggerStatement") +} + +pp.parseDoStatement = function(node) { + this.next() + this.labels.push(loopLabel) + node.body = this.parseStatement(false) + this.labels.pop() + this.expect(tt._while) + node.test = this.parseParenExpression() + if (this.options.ecmaVersion >= 6) + this.eat(tt.semi) + else + this.semicolon() + return this.finishNode(node, "DoWhileStatement") +} + +// Disambiguating between a `for` and a `for`/`in` or `for`/`of` +// loop is non-trivial. Basically, we have to parse the init `var` +// statement or expression, disallowing the `in` operator (see +// the second parameter to `parseExpression`), and then check +// whether the next token is `in` or `of`. When there is no init +// part (semicolon immediately after the opening parenthesis), it +// is a regular `for` loop. + +pp.parseForStatement = function(node) { + this.next() + let awaitAt = (this.options.ecmaVersion >= 9 && this.inAsync && this.eatContextual("await")) ? this.lastTokStart : -1 + this.labels.push(loopLabel) + this.enterLexicalScope() + this.expect(tt.parenL) + if (this.type === tt.semi) { + if (awaitAt > -1) this.unexpected(awaitAt) + return this.parseFor(node, null) + } + let isLet = this.isLet() + if (this.type === tt._var || this.type === tt._const || isLet) { + let init = this.startNode(), kind = isLet ? "let" : this.value + this.next() + this.parseVar(init, true, kind) + this.finishNode(init, "VariableDeclaration") + if ((this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual("of"))) && init.declarations.length === 1 && + !(kind !== "var" && init.declarations[0].init)) { + if (this.options.ecmaVersion >= 9) { + if (this.type === tt._in) { + if (awaitAt > -1) this.unexpected(awaitAt) + } else node.await = awaitAt > -1 + } + return this.parseForIn(node, init) + } + if (awaitAt > -1) this.unexpected(awaitAt) + return this.parseFor(node, init) + } + let refDestructuringErrors = new DestructuringErrors + let init = this.parseExpression(true, refDestructuringErrors) + if (this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual("of"))) { + if (this.options.ecmaVersion >= 9) { + if (this.type === tt._in) { + if (awaitAt > -1) this.unexpected(awaitAt) + } else node.await = awaitAt > -1 + } + this.toAssignable(init, false, refDestructuringErrors) + this.checkLVal(init) + return this.parseForIn(node, init) + } else { + this.checkExpressionErrors(refDestructuringErrors, true) + } + if (awaitAt > -1) this.unexpected(awaitAt) + return this.parseFor(node, init) +} + +pp.parseFunctionStatement = function(node, isAsync) { + this.next() + return this.parseFunction(node, true, false, isAsync) +} + +pp.parseIfStatement = function(node) { + this.next() + node.test = this.parseParenExpression() + // allow function declarations in branches, but only in non-strict mode + node.consequent = this.parseStatement(!this.strict && this.type == tt._function) + node.alternate = this.eat(tt._else) ? this.parseStatement(!this.strict && this.type == tt._function) : null + return this.finishNode(node, "IfStatement") +} + +pp.parseReturnStatement = function(node) { + if (!this.inFunction && !this.options.allowReturnOutsideFunction) + this.raise(this.start, "'return' outside of function") + this.next() + + // In `return` (and `break`/`continue`), the keywords with + // optional arguments, we eagerly look for a semicolon or the + // possibility to insert one. + + if (this.eat(tt.semi) || this.insertSemicolon()) node.argument = null + else { node.argument = this.parseExpression(); this.semicolon() } + return this.finishNode(node, "ReturnStatement") +} + +pp.parseSwitchStatement = function(node) { + this.next() + node.discriminant = this.parseParenExpression() + node.cases = [] + this.expect(tt.braceL) + this.labels.push(switchLabel) + this.enterLexicalScope() + + // Statements under must be grouped (by label) in SwitchCase + // nodes. `cur` is used to keep the node that we are currently + // adding statements to. + + let cur + for (let sawDefault = false; this.type != tt.braceR;) { + if (this.type === tt._case || this.type === tt._default) { + let isCase = this.type === tt._case + if (cur) this.finishNode(cur, "SwitchCase") + node.cases.push(cur = this.startNode()) + cur.consequent = [] + this.next() + if (isCase) { + cur.test = this.parseExpression() + } else { + if (sawDefault) this.raiseRecoverable(this.lastTokStart, "Multiple default clauses") + sawDefault = true + cur.test = null + } + this.expect(tt.colon) + } else { + if (!cur) this.unexpected() + cur.consequent.push(this.parseStatement(true)) + } + } + this.exitLexicalScope() + if (cur) this.finishNode(cur, "SwitchCase") + this.next() // Closing brace + this.labels.pop() + return this.finishNode(node, "SwitchStatement") +} + +pp.parseThrowStatement = function(node) { + this.next() + if (lineBreak.test(this.input.slice(this.lastTokEnd, this.start))) + this.raise(this.lastTokEnd, "Illegal newline after throw") + node.argument = this.parseExpression() + this.semicolon() + return this.finishNode(node, "ThrowStatement") +} + +// Reused empty array added for node fields that are always empty. + +const empty = [] + +pp.parseTryStatement = function(node) { + this.next() + node.block = this.parseBlock() + node.handler = null + if (this.type === tt._catch) { + let clause = this.startNode() + this.next() + this.expect(tt.parenL) + clause.param = this.parseBindingAtom() + this.enterLexicalScope() + this.checkLVal(clause.param, "let") + this.expect(tt.parenR) + clause.body = this.parseBlock(false) + this.exitLexicalScope() + node.handler = this.finishNode(clause, "CatchClause") + } + node.finalizer = this.eat(tt._finally) ? this.parseBlock() : null + if (!node.handler && !node.finalizer) + this.raise(node.start, "Missing catch or finally clause") + return this.finishNode(node, "TryStatement") +} + +pp.parseVarStatement = function(node, kind) { + this.next() + this.parseVar(node, false, kind) + this.semicolon() + return this.finishNode(node, "VariableDeclaration") +} + +pp.parseWhileStatement = function(node) { + this.next() + node.test = this.parseParenExpression() + this.labels.push(loopLabel) + node.body = this.parseStatement(false) + this.labels.pop() + return this.finishNode(node, "WhileStatement") +} + +pp.parseWithStatement = function(node) { + if (this.strict) this.raise(this.start, "'with' in strict mode") + this.next() + node.object = this.parseParenExpression() + node.body = this.parseStatement(false) + return this.finishNode(node, "WithStatement") +} + +pp.parseEmptyStatement = function(node) { + this.next() + return this.finishNode(node, "EmptyStatement") +} + +pp.parseLabeledStatement = function(node, maybeName, expr) { + for (let label of this.labels) + if (label.name === maybeName) + this.raise(expr.start, "Label '" + maybeName + "' is already declared") + let kind = this.type.isLoop ? "loop" : this.type === tt._switch ? "switch" : null + for (let i = this.labels.length - 1; i >= 0; i--) { + let label = this.labels[i] + if (label.statementStart == node.start) { + // Update information about previous labels on this node + label.statementStart = this.start + label.kind = kind + } else break + } + this.labels.push({name: maybeName, kind, statementStart: this.start}) + node.body = this.parseStatement(true) + if (node.body.type == "ClassDeclaration" || + node.body.type == "VariableDeclaration" && node.body.kind != "var" || + node.body.type == "FunctionDeclaration" && (this.strict || node.body.generator)) + this.raiseRecoverable(node.body.start, "Invalid labeled declaration") + this.labels.pop() + node.label = expr + return this.finishNode(node, "LabeledStatement") +} + +pp.parseExpressionStatement = function(node, expr) { + node.expression = expr + this.semicolon() + return this.finishNode(node, "ExpressionStatement") +} + +// Parse a semicolon-enclosed block of statements, handling `"use +// strict"` declarations when `allowStrict` is true (used for +// function bodies). + +pp.parseBlock = function(createNewLexicalScope = true) { + let node = this.startNode() + node.body = [] + this.expect(tt.braceL) + if (createNewLexicalScope) { + this.enterLexicalScope() + } + while (!this.eat(tt.braceR)) { + let stmt = this.parseStatement(true) + node.body.push(stmt) + } + if (createNewLexicalScope) { + this.exitLexicalScope() + } + return this.finishNode(node, "BlockStatement") +} + +// Parse a regular `for` loop. The disambiguation code in +// `parseStatement` will already have parsed the init statement or +// expression. + +pp.parseFor = function(node, init) { + node.init = init + this.expect(tt.semi) + node.test = this.type === tt.semi ? null : this.parseExpression() + this.expect(tt.semi) + node.update = this.type === tt.parenR ? null : this.parseExpression() + this.expect(tt.parenR) + this.exitLexicalScope() + node.body = this.parseStatement(false) + this.labels.pop() + return this.finishNode(node, "ForStatement") +} + +// Parse a `for`/`in` and `for`/`of` loop, which are almost +// same from parser's perspective. + +pp.parseForIn = function(node, init) { + let type = this.type === tt._in ? "ForInStatement" : "ForOfStatement" + this.next() + if (type == "ForInStatement") { + if (init.type === "AssignmentPattern" || + (init.type === "VariableDeclaration" && init.declarations[0].init != null && + (this.strict || init.declarations[0].id.type !== "Identifier"))) + this.raise(init.start, "Invalid assignment in for-in loop head") + } + node.left = init + node.right = type == "ForInStatement" ? this.parseExpression() : this.parseMaybeAssign() + this.expect(tt.parenR) + this.exitLexicalScope() + node.body = this.parseStatement(false) + this.labels.pop() + return this.finishNode(node, type) +} + +// Parse a list of variable declarations. + +pp.parseVar = function(node, isFor, kind) { + node.declarations = [] + node.kind = kind + for (;;) { + let decl = this.startNode() + this.parseVarId(decl, kind) + if (this.eat(tt.eq)) { + decl.init = this.parseMaybeAssign(isFor) + } else if (kind === "const" && !(this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual("of")))) { + this.unexpected() + } else if (decl.id.type != "Identifier" && !(isFor && (this.type === tt._in || this.isContextual("of")))) { + this.raise(this.lastTokEnd, "Complex binding patterns require an initialization value") + } else { + decl.init = null + } + node.declarations.push(this.finishNode(decl, "VariableDeclarator")) + if (!this.eat(tt.comma)) break + } + return node +} + +pp.parseVarId = function(decl, kind) { + decl.id = this.parseBindingAtom(kind) + this.checkLVal(decl.id, kind, false) +} + +// Parse a function declaration or literal (depending on the +// `isStatement` parameter). + +pp.parseFunction = function(node, isStatement, allowExpressionBody, isAsync) { + this.initFunction(node) + if (this.options.ecmaVersion >= 9 || this.options.ecmaVersion >= 6 && !isAsync) + node.generator = this.eat(tt.star) + if (this.options.ecmaVersion >= 8) + node.async = !!isAsync + + if (isStatement) { + node.id = isStatement === "nullableID" && this.type != tt.name ? null : this.parseIdent() + if (node.id) { + this.checkLVal(node.id, "var") + } + } + + let oldInGen = this.inGenerator, oldInAsync = this.inAsync, + oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldInFunc = this.inFunction + this.inGenerator = node.generator + this.inAsync = node.async + this.yieldPos = 0 + this.awaitPos = 0 + this.inFunction = true + this.enterFunctionScope() + + if (!isStatement) + node.id = this.type == tt.name ? this.parseIdent() : null + + this.parseFunctionParams(node) + this.parseFunctionBody(node, allowExpressionBody) + + this.inGenerator = oldInGen + this.inAsync = oldInAsync + this.yieldPos = oldYieldPos + this.awaitPos = oldAwaitPos + this.inFunction = oldInFunc + return this.finishNode(node, isStatement ? "FunctionDeclaration" : "FunctionExpression") +} + +pp.parseFunctionParams = function(node) { + this.expect(tt.parenL) + node.params = this.parseBindingList(tt.parenR, false, this.options.ecmaVersion >= 8) + this.checkYieldAwaitInDefaultParams() +} + +// Parse a class declaration or literal (depending on the +// `isStatement` parameter). + +pp.parseClass = function(node, isStatement) { + this.next() + + this.parseClassId(node, isStatement) + this.parseClassSuper(node) + let classBody = this.startNode() + let hadConstructor = false + classBody.body = [] + this.expect(tt.braceL) + while (!this.eat(tt.braceR)) { + const member = this.parseClassMember(classBody) + if (member && member.type === "MethodDefinition" && member.kind === "constructor") { + if (hadConstructor) this.raise(member.start, "Duplicate constructor in the same class") + hadConstructor = true + } + } + node.body = this.finishNode(classBody, "ClassBody") + return this.finishNode(node, isStatement ? "ClassDeclaration" : "ClassExpression") +} + +pp.parseClassMember = function(classBody) { + if (this.eat(tt.semi)) return null + + let method = this.startNode() + const tryContextual = (k, noLineBreak = false) => { + const start = this.start, startLoc = this.startLoc + if (!this.eatContextual(k)) return false + if (this.type !== tt.parenL && (!noLineBreak || !this.canInsertSemicolon())) return true + if (method.key) this.unexpected() + method.computed = false + method.key = this.startNodeAt(start, startLoc) + method.key.name = k + this.finishNode(method.key, "Identifier") + return false + } + + method.kind = "method" + method.static = tryContextual("static") + let isGenerator = this.eat(tt.star) + let isAsync = false + if (!isGenerator) { + if (this.options.ecmaVersion >= 8 && tryContextual("async", true)) { + isAsync = true + isGenerator = this.options.ecmaVersion >= 9 && this.eat(tt.star) + } else if (tryContextual("get")) { + method.kind = "get" + } else if (tryContextual("set")) { + method.kind = "set" + } + } + if (!method.key) this.parsePropertyName(method) + let {key} = method + if (!method.computed && !method.static && (key.type === "Identifier" && key.name === "constructor" || + key.type === "Literal" && key.value === "constructor")) { + if (method.kind !== "method") this.raise(key.start, "Constructor can't have get/set modifier") + if (isGenerator) this.raise(key.start, "Constructor can't be a generator") + if (isAsync) this.raise(key.start, "Constructor can't be an async method") + method.kind = "constructor" + } else if (method.static && key.type === "Identifier" && key.name === "prototype") { + this.raise(key.start, "Classes may not have a static property named prototype") + } + this.parseClassMethod(classBody, method, isGenerator, isAsync) + if (method.kind === "get" && method.value.params.length !== 0) + this.raiseRecoverable(method.value.start, "getter should have no params") + if (method.kind === "set" && method.value.params.length !== 1) + this.raiseRecoverable(method.value.start, "setter should have exactly one param") + if (method.kind === "set" && method.value.params[0].type === "RestElement") + this.raiseRecoverable(method.value.params[0].start, "Setter cannot use rest params") + return method +} + +pp.parseClassMethod = function(classBody, method, isGenerator, isAsync) { + method.value = this.parseMethod(isGenerator, isAsync) + classBody.body.push(this.finishNode(method, "MethodDefinition")) +} + +pp.parseClassId = function(node, isStatement) { + node.id = this.type === tt.name ? this.parseIdent() : isStatement === true ? this.unexpected() : null +} + +pp.parseClassSuper = function(node) { + node.superClass = this.eat(tt._extends) ? this.parseExprSubscripts() : null +} + +// Parses module export declaration. + +pp.parseExport = function(node, exports) { + this.next() + // export * from '...' + if (this.eat(tt.star)) { + this.expectContextual("from") + if (this.type !== tt.string) this.unexpected() + node.source = this.parseExprAtom() + this.semicolon() + return this.finishNode(node, "ExportAllDeclaration") + } + if (this.eat(tt._default)) { // export default ... + this.checkExport(exports, "default", this.lastTokStart) + let isAsync + if (this.type === tt._function || (isAsync = this.isAsyncFunction())) { + let fNode = this.startNode() + this.next() + if (isAsync) this.next() + node.declaration = this.parseFunction(fNode, "nullableID", false, isAsync) + } else if (this.type === tt._class) { + let cNode = this.startNode() + node.declaration = this.parseClass(cNode, "nullableID") + } else { + node.declaration = this.parseMaybeAssign() + this.semicolon() + } + return this.finishNode(node, "ExportDefaultDeclaration") + } + // export var|const|let|function|class ... + if (this.shouldParseExportStatement()) { + node.declaration = this.parseStatement(true) + if (node.declaration.type === "VariableDeclaration") + this.checkVariableExport(exports, node.declaration.declarations) + else + this.checkExport(exports, node.declaration.id.name, node.declaration.id.start) + node.specifiers = [] + node.source = null + } else { // export { x, y as z } [from '...'] + node.declaration = null + node.specifiers = this.parseExportSpecifiers(exports) + if (this.eatContextual("from")) { + if (this.type !== tt.string) this.unexpected() + node.source = this.parseExprAtom() + } else { + // check for keywords used as local names + for (let spec of node.specifiers) { + this.checkUnreserved(spec.local) + } + + node.source = null + } + this.semicolon() + } + return this.finishNode(node, "ExportNamedDeclaration") +} + +pp.checkExport = function(exports, name, pos) { + if (!exports) return + if (has(exports, name)) + this.raiseRecoverable(pos, "Duplicate export '" + name + "'") + exports[name] = true +} + +pp.checkPatternExport = function(exports, pat) { + let type = pat.type + if (type == "Identifier") + this.checkExport(exports, pat.name, pat.start) + else if (type == "ObjectPattern") + for (let prop of pat.properties) + this.checkPatternExport(exports, prop) + else if (type == "ArrayPattern") + for (let elt of pat.elements) { + if (elt) this.checkPatternExport(exports, elt) + } + else if (type == "Property") + this.checkPatternExport(exports, pat.value) + else if (type == "AssignmentPattern") + this.checkPatternExport(exports, pat.left) + else if (type == "RestElement") + this.checkPatternExport(exports, pat.argument) + else if (type == "ParenthesizedExpression") + this.checkPatternExport(exports, pat.expression) +} + +pp.checkVariableExport = function(exports, decls) { + if (!exports) return + for (let decl of decls) + this.checkPatternExport(exports, decl.id) +} + +pp.shouldParseExportStatement = function() { + return this.type.keyword === "var" || + this.type.keyword === "const" || + this.type.keyword === "class" || + this.type.keyword === "function" || + this.isLet() || + this.isAsyncFunction() +} + +// Parses a comma-separated list of module exports. + +pp.parseExportSpecifiers = function(exports) { + let nodes = [], first = true + // export { x, y as z } [from '...'] + this.expect(tt.braceL) + while (!this.eat(tt.braceR)) { + if (!first) { + this.expect(tt.comma) + if (this.afterTrailingComma(tt.braceR)) break + } else first = false + + let node = this.startNode() + node.local = this.parseIdent(true) + node.exported = this.eatContextual("as") ? this.parseIdent(true) : node.local + this.checkExport(exports, node.exported.name, node.exported.start) + nodes.push(this.finishNode(node, "ExportSpecifier")) + } + return nodes +} + +// Parses import declaration. + +pp.parseImport = function(node) { + this.next() + // import '...' + if (this.type === tt.string) { + node.specifiers = empty + node.source = this.parseExprAtom() + } else { + node.specifiers = this.parseImportSpecifiers() + this.expectContextual("from") + node.source = this.type === tt.string ? this.parseExprAtom() : this.unexpected() + } + this.semicolon() + return this.finishNode(node, "ImportDeclaration") +} + +// Parses a comma-separated list of module imports. + +pp.parseImportSpecifiers = function() { + let nodes = [], first = true + if (this.type === tt.name) { + // import defaultObj, { x, y as z } from '...' + let node = this.startNode() + node.local = this.parseIdent() + this.checkLVal(node.local, "let") + nodes.push(this.finishNode(node, "ImportDefaultSpecifier")) + if (!this.eat(tt.comma)) return nodes + } + if (this.type === tt.star) { + let node = this.startNode() + this.next() + this.expectContextual("as") + node.local = this.parseIdent() + this.checkLVal(node.local, "let") + nodes.push(this.finishNode(node, "ImportNamespaceSpecifier")) + return nodes + } + this.expect(tt.braceL) + while (!this.eat(tt.braceR)) { + if (!first) { + this.expect(tt.comma) + if (this.afterTrailingComma(tt.braceR)) break + } else first = false + + let node = this.startNode() + node.imported = this.parseIdent(true) + if (this.eatContextual("as")) { + node.local = this.parseIdent() + } else { + this.checkUnreserved(node.imported) + node.local = node.imported + } + this.checkLVal(node.local, "let") + nodes.push(this.finishNode(node, "ImportSpecifier")) + } + return nodes +} + +// Set `ExpressionStatement#directive` property for directive prologues. +pp.adaptDirectivePrologue = function(statements) { + for (let i = 0; i < statements.length && this.isDirectiveCandidate(statements[i]); ++i) { + statements[i].directive = statements[i].expression.raw.slice(1, -1) + } +} +pp.isDirectiveCandidate = function(statement) { + return ( + statement.type === "ExpressionStatement" && + statement.expression.type === "Literal" && + typeof statement.expression.value === "string" && + // Reject parenthesized strings. + (this.input[statement.start] === "\"" || this.input[statement.start] === "'") + ) +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/tokencontext.js b/tools/tests/apps/modules/imports/links/acorn/src/tokencontext.js new file mode 100644 index 0000000000..18dda85ce6 --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/tokencontext.js @@ -0,0 +1,148 @@ +// The algorithm used to determine whether a regexp can appear at a +// given point in the program is loosely based on sweet.js' approach. +// See https://github.com/mozilla/sweet.js/wiki/design + +import {Parser} from "./state" +import {types as tt} from "./tokentype" +import {lineBreak} from "./whitespace" + +export class TokContext { + constructor(token, isExpr, preserveSpace, override, generator) { + this.token = token + this.isExpr = !!isExpr + this.preserveSpace = !!preserveSpace + this.override = override + this.generator = !!generator + } +} + +export const types = { + b_stat: new TokContext("{", false), + b_expr: new TokContext("{", true), + b_tmpl: new TokContext("${", false), + p_stat: new TokContext("(", false), + p_expr: new TokContext("(", true), + q_tmpl: new TokContext("`", true, true, p => p.tryReadTemplateToken()), + f_stat: new TokContext("function", false), + f_expr: new TokContext("function", true), + f_expr_gen: new TokContext("function", true, false, null, true), + f_gen: new TokContext("function", false, false, null, true) +} + +const pp = Parser.prototype + +pp.initialContext = function() { + return [types.b_stat] +} + +pp.braceIsBlock = function(prevType) { + let parent = this.curContext() + if (parent === types.f_expr || parent === types.f_stat) + return true + if (prevType === tt.colon && (parent === types.b_stat || parent === types.b_expr)) + return !parent.isExpr + + // The check for `tt.name && exprAllowed` detects whether we are + // after a `yield` or `of` construct. See the `updateContext` for + // `tt.name`. + if (prevType === tt._return || prevType == tt.name && this.exprAllowed) + return lineBreak.test(this.input.slice(this.lastTokEnd, this.start)) + if (prevType === tt._else || prevType === tt.semi || prevType === tt.eof || prevType === tt.parenR || prevType == tt.arrow) + return true + if (prevType == tt.braceL) + return parent === types.b_stat + if (prevType == tt._var || prevType == tt.name) + return false + return !this.exprAllowed +} + +pp.inGeneratorContext = function() { + for (let i = this.context.length - 1; i >= 1; i--) { + let context = this.context[i] + if (context.token === "function") + return context.generator + } + return false +} + +pp.updateContext = function(prevType) { + let update, type = this.type + if (type.keyword && prevType == tt.dot) + this.exprAllowed = false + else if (update = type.updateContext) + update.call(this, prevType) + else + this.exprAllowed = type.beforeExpr +} + +// Token-specific context update code + +tt.parenR.updateContext = tt.braceR.updateContext = function() { + if (this.context.length == 1) { + this.exprAllowed = true + return + } + let out = this.context.pop() + if (out === types.b_stat && this.curContext().token === "function") { + out = this.context.pop() + } + this.exprAllowed = !out.isExpr +} + +tt.braceL.updateContext = function(prevType) { + this.context.push(this.braceIsBlock(prevType) ? types.b_stat : types.b_expr) + this.exprAllowed = true +} + +tt.dollarBraceL.updateContext = function() { + this.context.push(types.b_tmpl) + this.exprAllowed = true +} + +tt.parenL.updateContext = function(prevType) { + let statementParens = prevType === tt._if || prevType === tt._for || prevType === tt._with || prevType === tt._while + this.context.push(statementParens ? types.p_stat : types.p_expr) + this.exprAllowed = true +} + +tt.incDec.updateContext = function() { + // tokExprAllowed stays unchanged +} + +tt._function.updateContext = tt._class.updateContext = function(prevType) { + if (prevType.beforeExpr && prevType !== tt.semi && prevType !== tt._else && + !((prevType === tt.colon || prevType === tt.braceL) && this.curContext() === types.b_stat)) + this.context.push(types.f_expr) + else + this.context.push(types.f_stat) + this.exprAllowed = false +} + +tt.backQuote.updateContext = function() { + if (this.curContext() === types.q_tmpl) + this.context.pop() + else + this.context.push(types.q_tmpl) + this.exprAllowed = false +} + +tt.star.updateContext = function(prevType) { + if (prevType == tt._function) { + let index = this.context.length - 1 + if (this.context[index] === types.f_expr) + this.context[index] = types.f_expr_gen + else + this.context[index] = types.f_gen + } + this.exprAllowed = true +} + +tt.name.updateContext = function(prevType) { + let allowed = false + if (this.options.ecmaVersion >= 6) { + if (this.value == "of" && !this.exprAllowed || + this.value == "yield" && this.inGeneratorContext()) + allowed = true + } + this.exprAllowed = allowed +} diff --git a/tools/tests/apps/modules/imports/links/acorn/src/tokenize.js b/tools/tests/apps/modules/imports/links/acorn/src/tokenize.js new file mode 100644 index 0000000000..3afb8386f2 --- /dev/null +++ b/tools/tests/apps/modules/imports/links/acorn/src/tokenize.js @@ -0,0 +1,698 @@ +import {isIdentifierStart, isIdentifierChar} from "./identifier" +import {types as tt, keywords as keywordTypes} from "./tokentype" +import {Parser} from "./state" +import {SourceLocation} from "./locutil" +import {RegExpValidationState} from "./regexp" +import {lineBreak, lineBreakG, isNewLine, nonASCIIwhitespace} from "./whitespace" + +// Object type used to represent tokens. Note that normally, tokens +// simply exist as properties on the parser object. This is only +// used for the onToken callback and the external tokenizer. + +export class Token { + constructor(p) { + this.type = p.type + this.value = p.value + this.start = p.start + this.end = p.end + if (p.options.locations) + this.loc = new SourceLocation(p, p.startLoc, p.endLoc) + if (p.options.ranges) + this.range = [p.start, p.end] + } +} + +// ## Tokenizer + +const pp = Parser.prototype + +// Move to the next token + +pp.next = function() { + if (this.options.onToken) + this.options.onToken(new Token(this)) + + this.lastTokEnd = this.end + this.lastTokStart = this.start + this.lastTokEndLoc = this.endLoc + this.lastTokStartLoc = this.startLoc + this.nextToken() +} + +pp.getToken = function() { + this.next() + return new Token(this) +} + +// If we're in an ES6 environment, make parsers iterable +if (typeof Symbol !== "undefined") + pp[Symbol.iterator] = function() { + return { + next: () => { + let token = this.getToken() + return { + done: token.type === tt.eof, + value: token + } + } + } + } + +// Toggle strict mode. Re-reads the next number or string to please +// pedantic tests (`"use strict"; 010;` should fail). + +pp.curContext = function() { + return this.context[this.context.length - 1] +} + +// Read a single token, updating the parser object's token-related +// properties. + +pp.nextToken = function() { + let curContext = this.curContext() + if (!curContext || !curContext.preserveSpace) this.skipSpace() + + this.start = this.pos + if (this.options.locations) this.startLoc = this.curPosition() + if (this.pos >= this.input.length) return this.finishToken(tt.eof) + + if (curContext.override) return curContext.override(this) + else this.readToken(this.fullCharCodeAtPos()) +} + +pp.readToken = function(code) { + // Identifier or keyword. '\uXXXX' sequences are allowed in + // identifiers, so '\' also dispatches to that. + if (isIdentifierStart(code, this.options.ecmaVersion >= 6) || code === 92 /* '\' */) + return this.readWord() + + return this.getTokenFromCode(code) +} + +pp.fullCharCodeAtPos = function() { + let code = this.input.charCodeAt(this.pos) + if (code <= 0xd7ff || code >= 0xe000) return code + let next = this.input.charCodeAt(this.pos + 1) + return (code << 10) + next - 0x35fdc00 +} + +pp.skipBlockComment = function() { + let startLoc = this.options.onComment && this.curPosition() + let start = this.pos, end = this.input.indexOf("*/", this.pos += 2) + if (end === -1) this.raise(this.pos - 2, "Unterminated comment") + this.pos = end + 2 + if (this.options.locations) { + lineBreakG.lastIndex = start + let match + while ((match = lineBreakG.exec(this.input)) && match.index < this.pos) { + ++this.curLine + this.lineStart = match.index + match[0].length + } + } + if (this.options.onComment) + this.options.onComment(true, this.input.slice(start + 2, end), start, this.pos, + startLoc, this.curPosition()) +} + +pp.skipLineComment = function(startSkip) { + let start = this.pos + let startLoc = this.options.onComment && this.curPosition() + let ch = this.input.charCodeAt(this.pos += startSkip) + while (this.pos < this.input.length && !isNewLine(ch)) { + ch = this.input.charCodeAt(++this.pos) + } + if (this.options.onComment) + this.options.onComment(false, this.input.slice(start + startSkip, this.pos), start, this.pos, + startLoc, this.curPosition()) +} + +// Called at the start of the parse and after every token. Skips +// whitespace and comments, and. + +pp.skipSpace = function() { + loop: while (this.pos < this.input.length) { + let ch = this.input.charCodeAt(this.pos) + switch (ch) { + case 32: case 160: // ' ' + ++this.pos + break + case 13: + if (this.input.charCodeAt(this.pos + 1) === 10) { + ++this.pos + } + case 10: case 8232: case 8233: + ++this.pos + if (this.options.locations) { + ++this.curLine + this.lineStart = this.pos + } + break + case 47: // '/' + switch (this.input.charCodeAt(this.pos + 1)) { + case 42: // '*' + this.skipBlockComment() + break + case 47: + this.skipLineComment(2) + break + default: + break loop + } + break + default: + if (ch > 8 && ch < 14 || ch >= 5760 && nonASCIIwhitespace.test(String.fromCharCode(ch))) { + ++this.pos + } else { + break loop + } + } + } +} + +// Called at the end of every token. Sets `end`, `val`, and +// maintains `context` and `exprAllowed`, and skips the space after +// the token, so that the next one's `start` will point at the +// right position. + +pp.finishToken = function(type, val) { + this.end = this.pos + if (this.options.locations) this.endLoc = this.curPosition() + let prevType = this.type + this.type = type + this.value = val + + this.updateContext(prevType) +} + +// ### Token reading + +// This is the function that is called to fetch the next token. It +// is somewhat obscure, because it works in character codes rather +// than characters, and because operator parsing has been inlined +// into it. +// +// All in the name of speed. +// +pp.readToken_dot = function() { + let next = this.input.charCodeAt(this.pos + 1) + if (next >= 48 && next <= 57) return this.readNumber(true) + let next2 = this.input.charCodeAt(this.pos + 2) + if (this.options.ecmaVersion >= 6 && next === 46 && next2 === 46) { // 46 = dot '.' + this.pos += 3 + return this.finishToken(tt.ellipsis) + } else { + ++this.pos + return this.finishToken(tt.dot) + } +} + +pp.readToken_slash = function() { // '/' + let next = this.input.charCodeAt(this.pos + 1) + if (this.exprAllowed) { ++this.pos; return this.readRegexp() } + if (next === 61) return this.finishOp(tt.assign, 2) + return this.finishOp(tt.slash, 1) +} + +pp.readToken_mult_modulo_exp = function(code) { // '%*' + let next = this.input.charCodeAt(this.pos + 1) + let size = 1 + let tokentype = code === 42 ? tt.star : tt.modulo + + // exponentiation operator ** and **= + if (this.options.ecmaVersion >= 7 && code == 42 && next === 42) { + ++size + tokentype = tt.starstar + next = this.input.charCodeAt(this.pos + 2) + } + + if (next === 61) return this.finishOp(tt.assign, size + 1) + return this.finishOp(tokentype, size) +} + +pp.readToken_pipe_amp = function(code) { // '|&' + let next = this.input.charCodeAt(this.pos + 1) + if (next === code) return this.finishOp(code === 124 ? tt.logicalOR : tt.logicalAND, 2) + if (next === 61) return this.finishOp(tt.assign, 2) + return this.finishOp(code === 124 ? tt.bitwiseOR : tt.bitwiseAND, 1) +} + +pp.readToken_caret = function() { // '^' + let next = this.input.charCodeAt(this.pos + 1) + if (next === 61) return this.finishOp(tt.assign, 2) + return this.finishOp(tt.bitwiseXOR, 1) +} + +pp.readToken_plus_min = function(code) { // '+-' + let next = this.input.charCodeAt(this.pos + 1) + if (next === code) { + if (next == 45 && !this.inModule && this.input.charCodeAt(this.pos + 2) == 62 && + (this.lastTokEnd === 0 || lineBreak.test(this.input.slice(this.lastTokEnd, this.pos)))) { + // A `-->` line comment + this.skipLineComment(3) + this.skipSpace() + return this.nextToken() + } + return this.finishOp(tt.incDec, 2) + } + if (next === 61) return this.finishOp(tt.assign, 2) + return this.finishOp(tt.plusMin, 1) +} + +pp.readToken_lt_gt = function(code) { // '<>' + let next = this.input.charCodeAt(this.pos + 1) + let size = 1 + if (next === code) { + size = code === 62 && this.input.charCodeAt(this.pos + 2) === 62 ? 3 : 2 + if (this.input.charCodeAt(this.pos + size) === 61) return this.finishOp(tt.assign, size + 1) + return this.finishOp(tt.bitShift, size) + } + if (next == 33 && code == 60 && !this.inModule && this.input.charCodeAt(this.pos + 2) == 45 && + this.input.charCodeAt(this.pos + 3) == 45) { + // `