Skip to content
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.25.1] - 2026-08-07

### Fixed
- Destructuring **default initializers** are now included in a binding's taint span. A binding's default (`const { a = compute() } = obj`, `const [a = fallbackVal] = arr`) is a real second dependency — it supplies the value when the destructured slot is `undefined` — but it lives on the pattern (LHS), disjoint from the mapped source (RHS), so the element-wise span recorded in 0.25.0 covered only the source and excluded the default. A symbol used *only* inside such a default therefore escaped `findTaintedSymbolsByUsage` — a false negative. The binding's span is now widened to cover the default expression as well as its mapped source. (Narrow in scope: only bites when a tainted symbol appears solely in a binding default and nowhere else in the file, but false negatives are always worth closing.)

## [0.25.0] - 2026-08-03

### Changed
Expand Down Expand Up @@ -392,6 +397,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Multi-stage Docker build
- Automated vendor upgrade workflow

[0.25.1]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.25.0...v0.25.1
[0.25.0]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.24.13...v0.25.0
[0.24.13]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.24.12...v0.24.13
[0.24.12]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.24.11...v0.24.12
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.25.0
0.25.1
33 changes: 33 additions & 0 deletions internal/tsparse/tsparse.go
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,17 @@ func collectBindings(pattern *ast.Node, init *ast.Node, fbStart, fbEnd int, text
}
idx++

// A binding default (`= expr`) is a second dependency: its value is used
// when the destructured slot is undefined. It sits on the pattern (LHS),
// disjoint from the mapped source (RHS), so widen the span to cover both —
// otherwise a symbol used only inside a default would escape taint detection.
if be.Initializer != nil {
ds := posToLine(scanner.SkipTrivia(text, be.Initializer.Pos()), lineMap)
de := posToLine(be.Initializer.End(), lineMap)
start = minLine(start, ds)
end = maxLine(end, de)
}
Comment on lines +546 to +555

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve enclosing default spans during recursive binding collection.

The widened span is lost for nested bindings. The recursive child mapping at Line 540-542 replaces the inherited start and end values with the child source span.

For const { a: { b } = fallback } = { a: { b: value } }, the recorded span for b excludes fallback. A tainted symbol used only by fallback can therefore remain undetected.

Keep ancestor default spans separate from mapped source spans. Union the ancestor defaults after selecting each child source. Add regression tests for nested object and array defaults.

🐛 Proposed fix direction
-func collectBindings(pattern *ast.Node, init *ast.Node, fbStart, fbEnd int, text string, lineMap []core.TextPos, out *[]boundBinding) {
+func collectBindings(pattern *ast.Node, init *ast.Node, fbStart, fbEnd, inheritedDefaultStart, inheritedDefaultEnd int, text string, lineMap []core.TextPos, out *[]boundBinding) {
...
-	collectBindings(name, vd.Initializer, fbStart, fbEnd, text, lineMap, &out)
+	collectBindings(name, vd.Initializer, fbStart, fbEnd, 0, 0, text, lineMap, &out)
...
+		childDefaultStart, childDefaultEnd := inheritedDefaultStart, inheritedDefaultEnd
+		start = minLine(start, inheritedDefaultStart)
+		end = maxLine(end, inheritedDefaultEnd)
 		if be.Initializer != nil {
...
+			childDefaultStart = minLine(childDefaultStart, ds)
+			childDefaultEnd = maxLine(childDefaultEnd, de)
...
-			collectBindings(en, src, start, end, text, lineMap, out)
+			collectBindings(en, src, start, end, childDefaultStart, childDefaultEnd, text, lineMap, out)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// A binding default (`= expr`) is a second dependency: its value is used
// when the destructured slot is undefined. It sits on the pattern (LHS),
// disjoint from the mapped source (RHS), so widen the span to cover both —
// otherwise a symbol used only inside a default would escape taint detection.
if be.Initializer != nil {
ds := posToLine(scanner.SkipTrivia(text, be.Initializer.Pos()), lineMap)
de := posToLine(be.Initializer.End(), lineMap)
start = minLine(start, ds)
end = maxLine(end, de)
}
childDefaultStart, childDefaultEnd := inheritedDefaultStart, inheritedDefaultEnd
start = minLine(start, inheritedDefaultStart)
end = maxLine(end, inheritedDefaultEnd)
// A binding default (`= expr`) is a second dependency: its value is used
// when the destructured slot is undefined. It sits on the pattern (LHS),
// disjoint from the mapped source (RHS), so widen the span to cover both —
// otherwise a symbol used only inside a default would escape taint detection.
if be.Initializer != nil {
ds := posToLine(scanner.SkipTrivia(text, be.Initializer.Pos()), lineMap)
de := posToLine(be.Initializer.End(), lineMap)
start = minLine(start, ds)
end = maxLine(end, de)
childDefaultStart = minLine(childDefaultStart, ds)
childDefaultEnd = maxLine(childDefaultEnd, de)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tsparse/tsparse.go` around lines 546 - 555, Update the recursive
binding collection logic around the child mapping and default-span handling so
ancestor default spans are retained separately from each child’s mapped source
span, then union those ancestor spans into every descendant binding range.
Preserve the existing child-source selection while ensuring nested object and
array defaults, including fallback expressions, remain covered; add regression
tests for both cases.


en := be.Name()
if en == nil {
continue
Expand Down Expand Up @@ -627,6 +638,28 @@ func propNameText(n *ast.Node) string {
return ""
}

// minLine / maxLine combine 1-based line numbers, treating 0 as "unset" so a
// missing span doesn't collapse the union to line 0.
func minLine(a, b int) int {
switch {
case a == 0:
return b
case b == 0:
return a
case a < b:
return a
default:
return b
}
}

func maxLine(a, b int) int {
if a > b {
return a
}
return b
}

// extractDynamicImports walks the full AST to find dynamic import() calls
// and adds them to the imports list.
//
Expand Down
Loading