Repository: babel/babel (TypeScript)

Issue:
[Bug]: Some issue in @babel/parser version 7.20.0
### 💻

- [ ] Would you like to work on a fix?

### How are you using Babel?

@babel/cli

### Input code

After we moved from version 7.19.6 to 7.20.0 we got those new errors:

SyntaxError: A decorated export must export a class declaration.
at instantiate /node_modules/@babel/parser/src/parse-error/credentials.ts:62:21

TypeError: Cannot read property 'before' of undefined
at checkSpacingBefore

Can you check and fix it, please?

### Configuration file name

package.json

### Configuration

.....
"devDependencies": {
    "@babel/cli": "7.12.10",
    "@babel/core": "7.12.10",
    "@babel/plugin-proposal-class-properties": "7.12.1",
    "@babel/plugin-proposal-decorators": "7.12.12",
    "@babel/plugin-proposal-export-default-from": "7.12.1",
    "@babel/plugin-proposal-export-namespace-from": "7.12.1",
    "@babel/plugin-proposal-function-bind": "7.12.1",
    "@babel/plugin-proposal-nullish-coalescing-operator": "7.12.1",
    "@babel/plugin-proposal-optional-chaining": "7.12.7",
    "@babel/plugin-syntax-dynamic-import": "7.8.3",
    "@babel/plugin-transform-runtime": "7.12.10",
    "@babel/preset-env": "7.12.11",
    "@babel/preset-react": "7.12.10",
    "@babel/register": "7.18.9",
    "babel-eslint": "10.0.3",
    "babel-plugin-istanbul": "5.2.0",
    "babel-plugin-module-resolver": "3.2.0",
    "@babel/runtime": "7.12.5",
    .....
  },
  ......

### Current and expected behavior

Current: @babel/parser version 7.20.0 not working.
Expected: @babel/parser version 7.20.0 should work as version 7.19.6 worked properly.

### Environment

- Babel version: 7.12.10
- Node: v14.18.3
- npm version: 6.14.15
- OS: Red Hat Enterprise Linux Server 7.6 (Maipo)
- Monorepo: no


### Possible solution

To revert some latest changes.

### Additional context

_No response_


Hints:
Hey @doronaz! We really appreciate you taking the time to report an issue. The collaborators on this project attempt to help as many people as possible, but we're a limited number of volunteers, so it's possible this won't be addressed swiftly.

If you need any help, or just have general Babel or JavaScript questions, we have a vibrant [Slack community](https://babeljs.slack.com) that typically always has someone willing to help. You can sign-up [here](https://slack.babeljs.io/) for an invite.
Can you post the input example? We are looking for a decorated export declarations (e.g. `@dec export ...` ). 

> TypeError: Cannot read property 'before' of undefined
at checkSpacingBefore

It seems to me this error is thrown from `eslint`, are you using `babel-eslint`? If so can you upgrade to `@babel/eslint-parser` since `babel-eslint` is no longer supported.
> Can you post the input example? We are looking for a decorated export declarations (e.g. `@dec export ...` ).
> 
> > TypeError: Cannot read property 'before' of undefined
> > at checkSpacingBefore
> 
> It seems to me this error is thrown from `eslint`, are you using `babel-eslint`? If so can you upgrade to `@babel/eslint-parser` since `babel-eslint` is no longer supported.

Hi,
I worked with Doron on this issue, so replying on his behalf.

Yes, old version of `eslint` & `babel-eslint`.
But this happened on all our pipelines, i.e older releases-versions with no code changes.
We also tried to `eslint-disable` the entire file pointed by this error, but it did nothing. i.e not a linter error.
I don't think a minor upgrade should cause such a breaking changes, specially when this is sub-dependency and we do not control the version of babel/parser being installed. So now during all our builds the version bumped from 7.19.6 to 7.20.0 and throw this error.

For now we could by-pass the problem by adding `babel-parser` as a direct dependency with a specific version - but again, all our old releases are effected by that and this is just a work-around.

Thanks,
Ben
 
I can totally understand you, but unfortunately we don't have a good idea either.
Technically this is not a breaking change, but a bug fix.
It seems that `babel-eslint` is having problems relying on this particular behavior.
I noticed that there are some similar questions, e.g.
https://github.com/babel/babel/issues/14043
So a possible solution is to update `babel-eslint` to `@babel/eslint-parser`.
Because `babel-eslint` has not been updated for three years, due to its special nature, any new features or bug fixes of `@babel/paser` may break it.
In this case, it's really hard for us to maintain compatibility with it.
> We also tried to eslint-disable the entire file pointed by this error, but it did nothing.

Can you share the file? The SyntaxError in the original issue is definitely a bug.
Maybe this is enough to reproduce the regression:
```js
parser.parse("@dec export class A {}", {
  plugins: ["decorators-legacy", "estree"]
})
```

I'm not at home right now, but later I'll verify it.
https://runkit.com/liuxingbaoyu/635ba60aa01c76000df16cf5
Yes, this can be reproduced!
> https://runkit.com/liuxingbaoyu/635ba60aa01c76000df16cf5 Yes, this can be reproduced!

This example is throwing because `export` is not expected in script sourceType. If you add `sourceType: "module"`, Babel parses it successfully as expected.

@tamirbenCA Please provide a reproducible example.
👋 
We've been facing the same issue over the weekend with many of our build pipelines breaking

I tried to put together a minimal repro -- https://github.com/max-mathieu/babel-7.20-issue

Validation fails with 7.20 but passes with latest from 7.19 due to `keyword-spacing` rule and some issue parsing the decorator

Hope this helps, happy to help further (though I have no idea where to start for the fix)
The `@decorator export default class {}` is considered legacy and not supported in the latest decorators proposal. As a workaround, and more importantly to be future-proof, please place the decorator _after_ export:

```js
export default @decorator class {}
```

@max-mathieu Thanks for the reproduction repo. This helps a lot. The error is thrown because the ESLint `keyword-spacing` assumes that the first token of a export declaration `export default ...` must be a keyword.

https://github.com/eslint/eslint/blob/8a159686f9d497262d573dd601855ce28362199b/lib/rules/keyword-spacing.js#L459-L462

Of course this is not correct in the case `@decorator export default class {}` where it starts with `@`. In Babel 7.19, the export declaration starts at `export` but the class declaration `class {}` starts at `@`. In Babel 7.20, both the export declaration and the class declaration start at `@`. (See also https://github.com/babel/babel/pull/15032#issuecomment-1290747790)

The ESLint's goal is to support standarized JavaScript and not crashing on any stage 3 syntax, e.g. the latest decorators proposal. However, since `@decorator export default class {}` is not supported by the latest proposal, there is no guarantee that the rule `keyword-spacing` will ever support such syntax.

I will talk with team to decide Babel's action item on this issue.
Thanks @JLHwung -- glad it helped!

I understand it's legacy, but then `legacy: true` (or `version: 'legacy'`) in `.babelrc` seems to not be a valid option anymore. We've used it because we wanted to be safe and not have to change all the code.

At least, the error is very hard to understand as such, I'd rather have a `Invalid decorator on line 7 in index.js` than an internal babel error :)

Anyways, I guess we'll switch up the order (and be future proof), as I prefer this over pegging some versions in package.json
@max-mathieu The legacy decorator is still valid and supported in Babel 7. But it will be gradually phased out after decorators proposal becomes stage 4.

Patch:
diff --git a/packages/babel-parser/src/plugins/estree.ts b/packages/babel-parser/src/plugins/estree.ts
--- a/packages/babel-parser/src/plugins/estree.ts
+++ b/packages/babel-parser/src/plugins/estree.ts
@@ -455,6 +455,7 @@ export default (superClass: typeof Parser) =>
       unfinished: Undone<N.AnyExport>,
       decorators: N.Decorator[] | null,
     ) {
+      const exportStartLoc = this.state.lastTokStartLoc;
       const node = super.parseExport(unfinished, decorators);
 
       switch (node.type) {
@@ -476,6 +477,27 @@ export default (superClass: typeof Parser) =>
             delete node.specifiers;
           }
 
+        // fallthrough
+        case "ExportDefaultDeclaration":
+          {
+            const { declaration } = node;
+            if (
+              declaration?.type === "ClassDeclaration" &&
+              declaration.decorators?.length > 0 &&
+              // decorator comes before export
+              declaration.start === node.start
+            ) {
+              this.resetStartLocation(
+                node,
+                // For compatibility with ESLint's keyword-spacing rule, which assumes that an
+                // export declaration must start with export.
+                // https://github.com/babel/babel/issues/15085
+                // Here we reset export declaration's start to be the start of the export token
+                exportStartLoc,
+              );
+            }
+          }
+
           break;
       }
 


Test patch:
diff --git a/eslint/babel-eslint-tests/test/integration/eslint/verify.js b/eslint/babel-eslint-tests/test/integration/eslint/verify.js
--- a/eslint/babel-eslint-tests/test/integration/eslint/verify.js
+++ b/eslint/babel-eslint-tests/test/integration/eslint/verify.js
@@ -2,6 +2,32 @@ import verifyAndAssertMessages from "../../helpers/verifyAndAssertMessages.js";
 import path from "path";
 import { fileURLToPath } from "url";
 
+function verifyDecoratorsLegacyAndAssertMessages(
+  code,
+  rules,
+  expectedMessages,
+  sourceType,
+) {
+  const overrideConfig = {
+    parserOptions: {
+      sourceType,
+      babelOptions: {
+        configFile: path.resolve(
+          path.dirname(fileURLToPath(import.meta.url)),
+          "../../../../babel-eslint-shared-fixtures/config/babel.config.decorators-legacy.js",
+        ),
+      },
+    },
+  };
+  return verifyAndAssertMessages(
+    code,
+    rules,
+    expectedMessages,
+    sourceType,
+    overrideConfig,
+  );
+}
+
 describe("verify", () => {
   it("arrow function support (issue #1)", () => {
     verifyAndAssertMessages("describe('stuff', () => {});");
@@ -1073,32 +1099,6 @@ describe("verify", () => {
   });
 
   describe("decorators #72 (legacy)", () => {
-    function verifyDecoratorsLegacyAndAssertMessages(
-      code,
-      rules,
-      expectedMessages,
-      sourceType,
-    ) {
-      const overrideConfig = {
-        parserOptions: {
-          sourceType,
-          babelOptions: {
-            configFile: path.resolve(
-              path.dirname(fileURLToPath(import.meta.url)),
-              "../../../../babel-eslint-shared-fixtures/config/babel.config.decorators-legacy.js",
-            ),
-          },
-        },
-      };
-      return verifyAndAssertMessages(
-        code,
-        rules,
-        expectedMessages,
-        sourceType,
-        overrideConfig,
-      );
-    }
-
     it("class declaration", () => {
       verifyDecoratorsLegacyAndAssertMessages(
         `
@@ -1247,6 +1247,16 @@ describe("verify", () => {
     });
   });
 
+  describe("decorators #15085 (legacy)", () => {
+    it("works with keyword-spacing rule", () => {
+      verifyDecoratorsLegacyAndAssertMessages(
+        "@dec export class C {}; @dec export default class {}",
+        { "keyword-spacing": 1 },
+        [],
+      );
+    });
+  });
+
   it("detects minimal no-unused-vars case #120", () => {
     verifyAndAssertMessages("var unused;", { "no-unused-vars": 1 }, [
       "1:5 'unused' is defined but never used. no-unused-vars",
diff --git a/packages/babel-parser/test/fixtures/estree/export/decorator-before-export/input.js b/packages/babel-parser/test/fixtures/estree/export/decorator-before-export/input.js
new file mode 100644
--- /dev/null
+++ b/packages/babel-parser/test/fixtures/estree/export/decorator-before-export/input.js
@@ -0,0 +1,2 @@
+@dec export class C {}
+@dec export default class {}
diff --git a/packages/babel-parser/test/fixtures/estree/export/decorator-before-export/options.json b/packages/babel-parser/test/fixtures/estree/export/decorator-before-export/options.json
new file mode 100644
--- /dev/null
+++ b/packages/babel-parser/test/fixtures/estree/export/decorator-before-export/options.json
@@ -0,0 +1,3 @@
+{
+  "plugins": ["estree", "decorators-legacy"]
+}
diff --git a/packages/babel-parser/test/fixtures/estree/export/decorator-before-export/output.json b/packages/babel-parser/test/fixtures/estree/export/decorator-before-export/output.json
new file mode 100644
--- /dev/null
+++ b/packages/babel-parser/test/fixtures/estree/export/decorator-before-export/output.json
@@ -0,0 +1,70 @@
+{
+  "type": "File",
+  "start":0,"end":51,"loc":{"start":{"line":1,"column":0},"end":{"line":2,"column":28}},
+  "program": {
+    "type": "Program",
+    "start":0,"end":51,"loc":{"start":{"line":1,"column":0},"end":{"line":2,"column":28}},
+    "sourceType": "module",
+    "interpreter": null,
+    "body": [
+      {
+        "type": "ExportNamedDeclaration",
+        "start":5,"end":22,"loc":{"start":{"line":1,"column":5},"end":{"line":1,"column":22}},
+        "specifiers": [],
+        "source": null,
+        "declaration": {
+          "type": "ClassDeclaration",
+          "start":0,"end":22,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":22}},
+          "id": {
+            "type": "Identifier",
+            "start":18,"end":19,"loc":{"start":{"line":1,"column":18},"end":{"line":1,"column":19},"identifierName":"C"},
+            "name": "C"
+          },
+          "superClass": null,
+          "body": {
+            "type": "ClassBody",
+            "start":20,"end":22,"loc":{"start":{"line":1,"column":20},"end":{"line":1,"column":22}},
+            "body": []
+          },
+          "decorators": [
+            {
+              "type": "Decorator",
+              "start":0,"end":4,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":4}},
+              "expression": {
+                "type": "Identifier",
+                "start":1,"end":4,"loc":{"start":{"line":1,"column":1},"end":{"line":1,"column":4},"identifierName":"dec"},
+                "name": "dec"
+              }
+            }
+          ]
+        }
+      },
+      {
+        "type": "ExportDefaultDeclaration",
+        "start":28,"end":51,"loc":{"start":{"line":2,"column":5},"end":{"line":2,"column":28}},
+        "declaration": {
+          "type": "ClassDeclaration",
+          "start":23,"end":51,"loc":{"start":{"line":2,"column":0},"end":{"line":2,"column":28}},
+          "id": null,
+          "superClass": null,
+          "body": {
+            "type": "ClassBody",
+            "start":49,"end":51,"loc":{"start":{"line":2,"column":26},"end":{"line":2,"column":28}},
+            "body": []
+          },
+          "decorators": [
+            {
+              "type": "Decorator",
+              "start":23,"end":27,"loc":{"start":{"line":2,"column":0},"end":{"line":2,"column":4}},
+              "expression": {
+                "type": "Identifier",
+                "start":24,"end":27,"loc":{"start":{"line":2,"column":1},"end":{"line":2,"column":4},"identifierName":"dec"},
+                "name": "dec"
+              }
+            }
+          ]
+        }
+      }
+    ]
+  }
+}