You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Refine fix to special-case AsExpression and SatisfiesExpression
Changed from a general reparsed node fallback to specifically handling
AsExpression and SatisfiesExpression. These are the only reparsed node
types where their .Expression child should be visited.
The fix allows these special nodes to be visited even when reparsed, and
sets an allowReparsed flag when navigating into them, which enables
visiting their reparsed children to reach identifiers from JSDoc type
assertions.
This targeted approach is more precise and maintains strict filtering
for all other reparsed nodes.
Co-authored-by: andrewbranch <[email protected]>
In this case, the `AsExpression` IS the only child (besides the JSDoc). When it's skipped, there's NO other path to the identifier.
124
124
125
-
## Step 5: The Solution
125
+
## Step 5: The Solution (Revised)
126
126
127
-
The issue is that when navigating into a JSDoc type assertion, we need to be able to navigate through reparsed nodes to reach the actual identifiers within them. The current code correctly skips reparsed nodes to avoid returning positions within JSDoc comments, but it doesn't handle the case where the reparsed node is the ONLY way to reach a real token.
127
+
After feedback from @andrewbranch, the solution was refined to be more targeted. Instead of a general fallback mechanism for all reparsed nodes, the fix specifically handles `AsExpression` and `SatisfiesExpression`.
128
128
129
-
Looking at `visitNodeList`, there's already special handling for reparsed nodes:
130
-
```go
131
-
if match && nodes[index].Flags&ast.NodeFlagsReparsed != 0 {
**Key Insight:** `AsExpression` and `SatisfiesExpression` are special cases among reparsed nodes. While most reparsed nodes are synthetic and exist outside the "real" tree with no real position in the file, these two node kinds can have the `Reparsed` flag when they come from JSDoc type assertions, but their `.Expression` child should still be visited.
139
130
140
-
The problem is that `visitNode` (used for single children) doesn't have this fallback logic. It just skips reparsed nodes unconditionally.
131
+
**The Fix:** Modify `visitNode` to special-case `AsExpression` and `SatisfiesExpression`:
141
132
142
-
**The Fix:** Modify `visitNode` to match the behavior of `visitNodeList`. When a reparsed node is encountered that would match the position, we should still process it if there are no non-reparsed alternatives. This allows navigation through JSDoc type assertions while maintaining the invariant that identifiers never appear in trivia.
133
+
1. Check if a node is `AsExpression` or `SatisfiesExpression` when deciding whether to skip reparsed nodes
134
+
2. When we navigate into one of these special nodes, set an `allowReparsed` flag that allows visiting all their reparsed children
135
+
3. This allows recursive navigation through the reparsed tree structure to reach the actual identifier
143
136
144
-
Specifically, the fix is to:
145
-
1. When `visitNode` encounters a reparsed node that matches, don't immediately skip it - instead, save it in a `reparsedNext` variable
146
-
2. After visiting all children, if no non-reparsed match was found (`next == nil`), use the reparsed match
147
-
3. Reset `reparsedNext` at the end of each iteration
137
+
The logic:
138
+
```go
139
+
// Skip reparsed nodes unless:
140
+
// 1. The node itself is AsExpression or SatisfiesExpression, OR
141
+
// 2. We're already inside an AsExpression or SatisfiesExpression (allowReparsed=true)
if node.Flags&ast.NodeFlagsReparsed == 0 || isSpecialReparsed || allowReparsed {
146
+
// Process the node
147
+
}
148
+
```
148
149
149
-
This way:
150
-
- Normal cases continue to prefer non-reparsed nodes (e.g., when a parameter has both a JSDoc type and a real identifier)
151
-
- JSDoc type assertion cases can navigate through the reparsed AsExpression to reach the identifier
152
-
- The invariant is maintained: we never return an identifier found in trivia
150
+
When we navigate into an `AsExpression` or `SatisfiesExpression`, we set `allowReparsed = true` for the next iteration, which allows their reparsed children (like the identifier) to be visited.
153
151
154
152
## Implementation
155
153
156
154
The changes made to `internal/astnav/tokens.go`:
157
155
158
-
1. Added `reparsedNext` variable to track reparsed node matches as a fallback
156
+
1. Added `allowReparsed` flag to track when we're inside an AsExpression or SatisfiesExpression
159
157
2. Modified `visitNode` to:
160
-
- Still test reparsed nodes
161
-
- Save matching reparsed nodes in `reparsedNext` instead of skipping them entirely
162
-
- Prefer non-reparsed nodes by only setting `next` for non-reparsed matches
163
-
3. Added logic after `VisitEachChildAndJSDoc` to use `reparsedNext` if `next` is nil
164
-
4. Reset both `next` and `reparsedNext` at the end of each loop iteration
158
+
- Allow AsExpression and SatisfiesExpression nodes even if they're reparsed
159
+
- Allow any reparsed node if `allowReparsed` is true (we're inside a special node)
160
+
3. Set `allowReparsed = true` when navigating into AsExpression or SatisfiesExpression
165
161
166
-
This maintains backward compatibility while fixing the JSDoc type assertion panic. The panic condition is kept intact - identifiers should never appear in trivia.
162
+
This targeted approach:
163
+
- Only affects the specific node types that need special handling
164
+
- Maintains the strict reparsed node filtering for all other cases
165
+
- Keeps the panic intact - identifiers should never appear in actual trivia
166
+
- Maintains backward compatibility with all existing code
167
167
168
168
## Testing
169
169
@@ -174,5 +174,6 @@ All existing tests pass, including:
174
174
175
175
The fix correctly handles:
176
176
- JSDoc type assertions like `/**@type {string}*/(x)`
177
+
- JSDoc satisfies expressions
177
178
- Regular JSDoc comments (unchanged behavior)
178
179
- All other token position lookups (unchanged behavior)
0 commit comments