Bug Fix: Invalid Syntax With Async Override In TypeScript ESLint
Hey everyone! Today, we're diving into a tricky bug we've encountered in the typescript-eslint project, specifically with the promise-function-async rule. This issue causes the fixer to generate invalid TypeScript syntax when dealing with the override keyword. Let's break down what's happening, why it's happening, and how we can expect it to be resolved.
The Problem: Invalid Syntax Generation
So, what's the fuss all about? Imagine you have a class that extends another class, and you're overriding a method. This is a common pattern in object-oriented programming, and TypeScript handles it gracefully with the override keyword. Now, if you're also using the promise-function-async rule in your ESLint configuration, you might run into a situation where the fixer suggests adding the async keyword. Sounds good so far, right?
The problem arises when the fixer places the async keyword before the override keyword. In TypeScript, this is a big no-no! The correct order should be override async, and placing async first results in a syntax error. This is definitely not what we want, guys!
To illustrate, here’s an example of the incorrect syntax generated by the fixer:
class Base {
 async foo() {
 return Promise.resolve(42);
 }
}
class Derived extends Base {
 async override foo() { // Invalid syntax!
 return Promise.resolve(2000);
 }
}
And here’s how the syntax should look:
class Base {
 async foo() {
 return Promise.resolve(42);
 }
}
class Derived extends Base {
 override async foo() { // Correct syntax
 return Promise.resolve(2000);
 }
}
See the difference? It might seem like a small thing, but it can cause your code to break and lead to some head-scratching moments. The correct ordering of modifiers is crucial for TypeScript to properly understand and compile your code. When the async keyword precedes override, TypeScript's compiler throws a syntax error, halting the compilation process and preventing your application from running as expected. This can be particularly frustrating in large projects where such syntax errors can be easily overlooked during code reviews, only to surface during runtime.
Why is This Happening?
Alright, let's get a bit technical and understand why this bug is occurring. The root cause lies in the fixer's token-skipping logic. The fixer is designed to analyze your code and suggest changes, but in this case, it's making a mistake in how it identifies the correct position for inserting the async keyword. Specifically, the issue stems from how the fixer handles different types of tokens in the TypeScript Abstract Syntax Tree (AST).
The fixer's current logic primarily focuses on handling Keyword type tokens, such as public and static. These keywords are correctly identified, and the fixer knows where to place the async keyword in relation to them. However, the override keyword in TypeScript is classified as an Identifier token, not a Keyword token. This distinction is crucial because the fixer's token-skipping mechanism doesn't properly account for Identifier tokens like override.
Because the fixer doesn't recognize override as a modifier in the same way it recognizes public or static, it miscalculates the insertion point for the async keyword. Instead of placing async after override, it incorrectly inserts it before, leading to the invalid syntax. This is a classic example of a bug arising from incomplete handling of language features in a code analysis tool. The AST, which represents the code's structure, is parsed differently for override compared to other modifiers, causing the fixer to stumble.
To further illustrate, consider how the AST represents the code. When the fixer encounters a modifier like public, it knows to look for the next appropriate place to insert async. However, when it encounters override (as an Identifier), it doesn't apply the same logic, leading to the incorrect insertion. This highlights the importance of comprehensive token handling in linters and fixers to ensure accurate and reliable code modifications.
The Technical Details: Token Types and AST
To really dig into the heart of the issue, we need to talk about token types and the Abstract Syntax Tree (AST). When your code is parsed, it's broken down into tokens, which are the basic building blocks of the language. These tokens have different types, such as Keyword, Identifier, Punctuator (like commas and semicolons), and more.
The AST is a tree-like representation of your code's structure, where each node in the tree corresponds to a construct in your code, such as a class, method, or variable declaration. The fixer uses the AST to understand the code's structure and make changes.
In this case, the problem is that the fixer's logic for skipping tokens only considers Keyword tokens, like public or static. But override is an Identifier token. This means the fixer doesn't correctly identify the position where async should be inserted, leading to the syntax error.
If you're curious to see the AST for yourself, you can use tools like AST Explorer. It's a fantastic way to visualize how your code is parsed and understand the structure that tools like ESLint and the TypeScript compiler work with. Understanding the AST can be incredibly helpful for debugging and contributing to projects like typescript-eslint.
The Solution: What's Being Done
The good news is that the team behind typescript-eslint is aware of this issue and is working on a fix. The key to resolving this bug is to update the fixer's token-skipping logic to correctly handle Identifier tokens, specifically the override keyword. This will ensure that the async keyword is inserted in the correct position, following TypeScript's modifier ordering rules.
The fix will likely involve modifying the code that determines where to insert the async keyword. Instead of only looking for Keyword tokens, the logic will need to be expanded to also recognize Identifier tokens like override. This might involve adding a check for Identifier tokens with a specific name (in this case, override) or updating the token-skipping algorithm to be more general and handle different token types appropriately.
Once the fix is implemented, it will be included in a future release of the @typescript-eslint/eslint-plugin package. So, if you're affected by this bug, keep an eye out for updates and make sure to upgrade your packages when the fix is available. This will ensure that your code is correctly formatted and that you're not running into any unexpected syntax errors.
Workarounds: What Can You Do in the Meantime?
While we wait for the official fix to be released, there are a few workarounds you can use to avoid this issue. These workarounds might not be ideal, but they can help you keep your code clean and error-free in the meantime.
1. Manually Apply the Fix
The simplest workaround is to manually apply the fix whenever the fixer generates the incorrect syntax. This means that after running ESLint and applying the suggested fixes, you'll need to go through your code and look for instances where async is placed before override. If you find any, simply swap the order of the keywords to override async. This is a bit tedious, but it ensures that your code is syntactically correct.
2. Disable the promise-function-async Rule Temporarily
If you find that the incorrect fixes are too frequent and manual correction is becoming a burden, you might consider temporarily disabling the promise-function-async rule. You can do this by removing the rule from your ESLint configuration or setting its severity to `