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.24.13] - 2026-08-01

### Fixed
- Destructuring variable declarations (`const { a, b } = expr` / `const [a, b] = expr`) are now decomposed into their individual bindings for both symbol and export extraction. Previously `getDeclName` only handled plain identifiers and returned `""` for binding patterns, so destructured names were registered as neither symbols nor exports — they simply didn't exist in the file model. That silently broke taint propagation through a very common pattern: e.g. `export const { store, startSagas, … } = createStore()` in `gdc-analytical-designer-runtime`'s `reduxStore.ts`. A runtime change to the reducers feeding `createStore` tainted `createStore`, but the taint could not reach `store`/`startSagas`/…, so the package reported **0 affected exports** and nothing propagated to the AD module/harness (or the dashboards harness that embeds it) — a false negative. Each destructured binding is now emitted as a symbol/export; renames (`{ a: b }` → `b`), rest elements (`...x`), nested patterns and array holes are handled. To stay precise, a binding's symbol span is the **initializer expression** (not the whole statement), so the sibling binding names don't bleed into the compared body and cross-link in the AST diff — each binding is attributed exactly to the initializer it destructures.

## [0.24.12] - 2026-07-30

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

[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
[0.24.11]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.24.10...v0.24.11
[0.24.10]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.24.9...v0.24.10
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.24.12
0.24.13
84 changes: 80 additions & 4 deletions internal/tsparse/tsparse.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,7 @@ func extractExports(stmt *ast.Node, analysis *FileAnalysis) {
dl := vs.DeclarationList.AsVariableDeclarationList()
if dl.Declarations != nil {
for _, decl := range dl.Declarations.Nodes {
name := getDeclName(decl)
if name != "" {
if name := getDeclName(decl); name != "" {
exportName := name
if isDefault {
exportName = "default"
Expand All @@ -246,6 +245,19 @@ func extractExports(stmt *ast.Node, analysis *FileAnalysis) {
Name: exportName,
LocalName: name,
})
continue
}
// Destructuring export: `export const { a, b } = init` binds each
// name locally; record them all (destructuring can't be `default`).
declName := decl.Name()
if declName == nil || !(ast.IsObjectBindingPattern(declName) || ast.IsArrayBindingPattern(declName)) {
continue
}
for _, name := range bindingPatternNames(declName) {
analysis.Exports = append(analysis.Exports, Export{
Name: name,
LocalName: name,
})
}
}
}
Expand Down Expand Up @@ -365,8 +377,7 @@ func extractDeclarations(stmt *ast.Node, lineMap []core.TextPos, analysis *FileA
dl := vs.DeclarationList.AsVariableDeclarationList()
if dl.Declarations != nil {
for _, decl := range dl.Declarations.Nodes {
name := getDeclName(decl)
if name != "" {
if name := getDeclName(decl); name != "" {
analysis.Symbols = append(analysis.Symbols, SymbolDecl{
Name: name,
Kind: "variable",
Expand All @@ -375,6 +386,30 @@ func extractDeclarations(stmt *ast.Node, lineMap []core.TextPos, analysis *FileA
IsExported: isExported,
ExportName: name,
})
continue
}
// Destructuring: `const { a, b } = init`. Attribute each bound name to
// the shared initializer expression's line span — NOT the whole statement,
// which would drag the sibling binding names into the body and cross-link
// them in the AST diff. Each binding genuinely depends on `init`.
declName := decl.Name()
if declName == nil || !(ast.IsObjectBindingPattern(declName) || ast.IsArrayBindingPattern(declName)) {
continue
}
startLine, endLine := declInitLines(decl, text, lineMap)
if startLine == 0 {
startLine = stmtStartLine(stmt, text, lineMap)
endLine = posToLine(stmt.End(), lineMap)
}
for _, name := range bindingPatternNames(declName) {
analysis.Symbols = append(analysis.Symbols, SymbolDecl{
Name: name,
Kind: "variable",
StartLine: startLine,
EndLine: endLine,
IsExported: isExported,
ExportName: name,
})
}
}
}
Expand All @@ -393,6 +428,47 @@ func getDeclName(node *ast.Node) string {
return ""
}

// bindingPatternNames returns every local identifier bound by an object/array
// destructuring pattern, following renames (`{ a: b }` yields `b`), rest
// elements (`...x`) and nested patterns; array holes (omitted elements) are
// skipped.
func bindingPatternNames(pattern *ast.Node) []string {
bp := pattern.AsBindingPattern()
if bp.Elements == nil {
return nil
}
var names []string
for _, elem := range bp.Elements.Nodes {
if !ast.IsBindingElement(elem) {
continue // array hole (OmittedExpression)
}
en := elem.AsBindingElement().Name()
if en == nil {
continue
}
switch {
case ast.IsIdentifier(en):
names = append(names, en.Text())
case ast.IsObjectBindingPattern(en) || ast.IsArrayBindingPattern(en):
names = append(names, bindingPatternNames(en)...)
}
}
return names
}

// declInitLines returns the 1-based [start, end] line span of a variable
// declaration's initializer expression (leading trivia skipped). Returns (0, 0)
// when the declaration has no initializer.
func declInitLines(decl *ast.Node, text string, lineMap []core.TextPos) (int, int) {
vd := decl.AsVariableDeclaration()
if vd.Initializer == nil {
return 0, 0
}
start := posToLine(scanner.SkipTrivia(text, vd.Initializer.Pos()), lineMap)
end := posToLine(vd.Initializer.End(), lineMap)
return start, end
}

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