From 0446604aed35a77d012d510c4533b773bf281032 Mon Sep 17 00:00:00 2001 From: Martin Najemi Date: Fri, 7 Aug 2026 17:13:52 +0200 Subject: [PATCH] fix: Include binding defaults in destructuring taint Risk: low --- CHANGELOG.md | 6 ++++++ VERSION | 2 +- internal/tsparse/tsparse.go | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9dfd42..15d852d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/VERSION b/VERSION index 94a5fe4..5d60147 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.25.0 \ No newline at end of file +0.25.1 \ No newline at end of file diff --git a/internal/tsparse/tsparse.go b/internal/tsparse/tsparse.go index 51eb68d..9f2c350 100644 --- a/internal/tsparse/tsparse.go +++ b/internal/tsparse/tsparse.go @@ -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) + } + en := be.Name() if en == nil { continue @@ -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. //