Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support ES2023 and hashbangs #556

Merged
merged 5 commits into from
Aug 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,17 +231,11 @@ We are building on top of Acorn, however, so that we can contribute back and hel

### What ECMAScript features do you support?

Espree supports all ECMAScript 2021 features and partially supports ECMAScript 2022 features.
Espree supports all ECMAScript 2022 features and partially supports ECMAScript 2023 features.

Because ECMAScript 2022 is still under development, we are implementing features as they are finalized. Currently, Espree supports:
Because ECMAScript 2023 is still under development, we are implementing features as they are finalized. Currently, Espree supports:

* [Class instance fields](https://github.com/tc39/proposal-class-fields)
* [Class private instance methods and accessors](https://github.com/tc39/proposal-private-methods)
* [Class static fields, static private methods and accessors](https://github.com/tc39/proposal-static-class-features)
* [RegExp match indices](https://github.com/tc39/proposal-regexp-match-indices)
* [Top-level await](https://github.com/tc39/proposal-top-level-await)
* [Class static initialization blocks](https://github.com/tc39/proposal-class-static-block)
* [Ergonomic brand checks for Private Fields](https://github.com/tc39/proposal-private-fields-in-in)
* [Hashbang grammar](https://github.com/tc39/proposal-hashbang)

See [finished-proposals.md](https://github.com/tc39/proposals/blob/master/finished-proposals.md) to know what features are finalized.

Expand Down
60 changes: 40 additions & 20 deletions lib/espree.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,23 @@ const ESPRIMA_FINISH_NODE = Symbol("espree's esprimaFinishNode");
* @param {int} end The index at which the comment ends.
* @param {Location} startLoc The location at which the comment starts.
* @param {Location} endLoc The location at which the comment ends.
* @param {string} code The source code being parsed.
* @returns {Object} The comment object.
* @private
*/
function convertAcornCommentToEsprimaComment(block, text, start, end, startLoc, endLoc) {
function convertAcornCommentToEsprimaComment(block, text, start, end, startLoc, endLoc, code) {
let type;

if (block) {
type = "Block";
} else if (code.slice(start, start + 2) === "#!") {
type = "Hashbang";
} else {
type = "Line";
}

const comment = {
type: block ? "Block" : "Line",
type,
value: text
};

Expand Down Expand Up @@ -65,6 +76,25 @@ export default () => Parser => {
? new TokenTranslator(tokTypes, code)
: null;

/*
* Data that is unique to Espree and is not represented internally
* in Acorn.
*
* For ES2023 hashbangs, Espree will call `onComment()` during the
* constructor, so we must define state before having access to
* `this`.
*/
const state = {
originalSourceType: originalSourceType || options.sourceType,
tokens: tokenTranslator ? [] : null,
comments: options.comment === true ? [] : null,
impliedStrict: ecmaFeatures.impliedStrict === true && options.ecmaVersion >= 5,
ecmaVersion: options.ecmaVersion,
jsxAttrValueToken: false,
lastToken: null,
templateElements: []
};

// Initialize acorn parser.
super({

Expand All @@ -83,38 +113,28 @@ export default () => Parser => {
if (tokenTranslator) {

// Use `tokens`, `ecmaVersion`, and `jsxAttrValueToken` in the state.
tokenTranslator.onToken(token, this[STATE]);
tokenTranslator.onToken(token, state);
}
if (token.type !== tokTypes.eof) {
this[STATE].lastToken = token;
state.lastToken = token;
}
},

// Collect comments
onComment: (block, text, start, end, startLoc, endLoc) => {
if (this[STATE].comments) {
const comment = convertAcornCommentToEsprimaComment(block, text, start, end, startLoc, endLoc);
if (state.comments) {
const comment = convertAcornCommentToEsprimaComment(block, text, start, end, startLoc, endLoc, code);

this[STATE].comments.push(comment);
state.comments.push(comment);
}
}
}, code);

/*
* Data that is unique to Espree and is not represented internally in
* Acorn. We put all of this data into a symbol property as a way to
* avoid potential naming conflicts with future versions of Acorn.
* We put all of this data into a symbol property as a way to avoid
* potential naming conflicts with future versions of Acorn.
*/
this[STATE] = {
originalSourceType: originalSourceType || options.sourceType,
tokens: tokenTranslator ? [] : null,
comments: options.comment === true ? [] : null,
impliedStrict: ecmaFeatures.impliedStrict === true && this.options.ecmaVersion >= 5,
ecmaVersion: this.options.ecmaVersion,
jsxAttrValueToken: false,
lastToken: null,
templateElements: []
};
this[STATE] = state;
}

tokenize() {
Expand Down
17 changes: 9 additions & 8 deletions lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
const SUPPORTED_VERSIONS = [
3,
5,
6,
7,
8,
9,
10,
11,
12,
13
6, // 2015
7, // 2016
8, // 2017
9, // 2018
10, // 2019
11, // 2020
12, // 2021
13, // 2022
14 // 2023
];

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
"index": 15,
"lineNumber": 2,
"column": 2,
"message": "Unexpected character '!'"
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
'use strict';
#!/usr/bin/env node
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export default {
"type": "Program",
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 2,
"column": 0
}
},
"range": [
0,
20
],
"body": [],
"sourceType": "script",
"comments": [
{
"type": "Hashbang",
"value": "/usr/bin/env node",
"range": [
0,
19
],
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 19
}
}
}
],
"tokens": []
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#!/usr/bin/env node
6 changes: 6 additions & 0 deletions tests/lib/ecma-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ describe("ecmaVersion", () => {

it("should parse correctly when sourceType is script", async () => {
config.ecmaVersion = Number(version);
if (filename.includes("comment")) {
config.comment = true;
}

const absolutePath = path.resolve(__dirname, FIXTURES_DIR, filename.slice(1));
// eslint-disable-next-line node/no-unsupported-features/es-syntax
Expand Down Expand Up @@ -107,6 +110,9 @@ describe("ecmaVersion", () => {

config.ecmaVersion = Number(version);
config.sourceType = "module";
if (filename.includes("comment")) {
config.comment = true;
}

// set sourceType of program node to module
if (expected.type === "Program") {
Expand Down
4 changes: 2 additions & 2 deletions tests/lib/supported-ecmaversions.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ import * as espree from "../../espree.js";

describe("latestEcmaVersion", () => {
it("should return the latest supported ecmaVersion", () => {
assert.strictEqual(espree.latestEcmaVersion, 13);
assert.strictEqual(espree.latestEcmaVersion, 14);
});
});

describe("supportedEcmaVersions", () => {
it("should return an array of all supported versions", () => {
assert.deepStrictEqual(
espree.supportedEcmaVersions,
[3, 5, 6, 7, 8, 9, 10, 11, 12, 13]
[3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
);
});
});
2 changes: 2 additions & 0 deletions tools/update-ecma-version-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,14 @@ function main() {
const moduleOnly = !scriptOnly && name.includes("modules");
const expectedToBeError = name.includes("/invalid-");
const expectedToBeOK = name.includes("/valid-");
const comment = name.includes("comment");
const sourceFilePath = `${path.resolve(rootDir, name)}.src.js`;
const resultFilePath = `${path.resolve(rootDir, name)}.result.js`;
const moduleResultFilePath = `${path.resolve(rootDir, name)}.module-result.js`;
const relSourceFilePath = path.relative(process.cwd(), sourceFilePath);
const code = shelljs.cat(sourceFilePath);
const parserOptions = {
comment,
loc: true,
range: true,
tokens: true,
Expand Down