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.5] - 2026-08-10

### Fixed
- Import-time side-effect taint now propagates **transitively** through import and re-export edges. A file whose change ran at import time (a changed top-level statement like `console.log(...)`, or a bare `import "x"`) tainted its *direct* importers, but the "runs at import time" quality was lost after one hop: a barrel that re-exports the side-effectful module (`export { … } from "./api/ai/ai.js"` in an entrypoint `index.ts`) only picked up the re-exported *symbols*, so consumers importing the barrel for *other* symbols were missed — e.g. a `console.log` added to `e2e-utils/src/api/ai/ai.ts` flagged only the 5 consumers of the `ai` exports instead of all 8 e2e-utils consumers (importing the barrel loads `ai.ts` and executes the statement regardless of which symbol is used). Such changes now carry a `__side-effect__` sentinel that flows through every import/re-export edge, marking each file it reaches as wholly tainted and itself side-effectful, so barrels become side-effectful and all their consumers are flagged — the same result as adding the statement to the entrypoint directly. This is deliberately assume-the-worst; a `TODO` in the propagation notes the follow-up to refine it using each package's `package.json` `"sideEffects"` field (a side-effect-free module is tree-shaken and should not propagate).

## [0.25.4] - 2026-08-07

### Fixed
Expand Down Expand Up @@ -413,6 +418,7 @@ Together these keep genuine import-time changes flagged while eliminating the la
- Multi-stage Docker build
- Automated vendor upgrade workflow

[0.25.5]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.25.4...v0.25.5
[0.25.4]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.25.3...v0.25.4
[0.25.3]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.25.2...v0.25.3
[0.25.2]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.25.1...v0.25.2
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.25.4
0.25.5
29 changes: 29 additions & 0 deletions internal/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,23 @@ func AnalyzeLibraryPackage(projectFolder string, entrypoints []Entrypoint, merge
}
}

// Import-time side-effect transitivity: if the imported module has an
// import-time side effect (sideEffectTaint), importing it re-runs that
// side effect here, so this file becomes side-effectful too — taint all
// its symbols and carry "*" + sideEffectTaint so it keeps flowing to this
// file's own importers/re-exporters (a barrel re-exporting a side-effectful
// module becomes side-effectful itself).
// TODO: make this precise using the "sideEffects" field in each package's
// package.json — a module marked side-effect-free is tree-shaken and not
// re-executed on import, so it should not propagate. Until then we assume
// the worst and propagate through every import/re-export edge. Follow-up.
if currentTainted[sideEffectTaint] {
for _, sym := range importerAnalysis.Symbols {
newlyTainted = append(newlyTainted, sym.Name)
}
newlyTainted = append(newlyTainted, "*", sideEffectTaint)
}
Comment on lines +786 to +801

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 | 🏗️ Heavy lift

Do not propagate import-time side effects through type-only re-export edges.

Both graph builders include exp.IsTypeOnly re-exports as import edges. If the source has "*" and sideEffectTaint, a statement such as export type { T } from "./source" reaches these blocks and taints all runtime symbols in the barrel. Type-only re-exports do not load ./source at runtime.

  • internal/analyzer/analyzer.go#L786-L801: apply side-effect propagation only when the edge represents a runtime import or re-export.
  • internal/analyzer/analyzer.go#L1644-L1654: apply the same runtime-edge condition in FindAffectedFiles.
📍 Affects 1 file
  • internal/analyzer/analyzer.go#L786-L801 (this comment)
  • internal/analyzer/analyzer.go#L1644-L1654
🤖 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/analyzer/analyzer.go` around lines 786 - 801, Restrict import-time
side-effect propagation to runtime import or re-export edges by checking the
edge’s non-type-only status before the currentTainted[sideEffectTaint] handling
in internal/analyzer/analyzer.go lines 786-801. Apply the same runtime-edge
condition in FindAffectedFiles at internal/analyzer/analyzer.go lines 1644-1654;
type-only re-exports must not taint barrel symbols or propagate sideEffectTaint.


// Named imports: find symbols that use the tainted imports
if len(taintedLocalNames) > 0 {
usageTainted := findTaintedSymbolsByUsage(importerAnalysis, taintedLocalNames)
Expand Down Expand Up @@ -1624,6 +1641,18 @@ func FindAffectedFiles(globPattern string, filterPattern string, upstreamTaint m
}
}

// Import-time side-effect transitivity — see the matching block in
// AnalyzeLibraryPackage. Importing a side-effectful module re-runs its
// side effect here, so this file becomes side-effectful too and keeps
// propagating it (assume-the-worst; refine later via package.json
// "sideEffects" — see that TODO).
if currentTainted[sideEffectTaint] {
for _, sym := range importerAnalysis.Symbols {
newlyTainted = append(newlyTainted, sym.Name)
}
newlyTainted = append(newlyTainted, "*", sideEffectTaint)
}

if len(taintedLocalNames) > 0 {
usageTainted := findTaintedSymbolsByUsage(importerAnalysis, taintedLocalNames)
newlyTainted = append(newlyTainted, usageTainted...)
Expand Down
17 changes: 13 additions & 4 deletions internal/analyzer/astdiff.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import (
"goodchanges/tsgo-vendor/pkg/scanner"
)

// sideEffectTaint is a sentinel taint token marking that a file has an *import-time
// side effect* change (a changed top-level side-effect statement, or a bare
// `import "x"` side-effect import) — as opposed to an ordinary whole-file "*" taint
// (e.g. a new file). It propagates through import/re-export edges so a barrel that
// re-exports a side-effectful module becomes side-effectful itself. It is not a
// valid JS identifier, so it can never collide with or be matched as a real symbol.
const sideEffectTaint = "__side-effect__"

// findAffectedSymbolsByASTDiff compares OLD and NEW file ASTs to find which symbols changed.
// Returns symbol names that have runtime changes (or type-only changes if includeTypes is true).
//
Expand Down Expand Up @@ -232,10 +240,11 @@ func findAffectedSymbolsByASTDiff(oldAnalysis *tsparse.FileAnalysis, newAnalysis
if hasSideEffectStmtChanges(oldAnalysis.SourceFile, newAnalysis.SourceFile) ||
bareImportsChanged(oldAnalysis, newAnalysis) {
log.Debugf(" file changed with import-time side effects — tainting all symbols")
// Use "*" wildcard to mark all exports as affected.
// This handles barrel/entrypoint files that have no symbol declarations
// but whose runtime side effects affect all importers.
affected = append(affected, "*")
// Use "*" wildcard to mark all exports as affected, plus the
// sideEffectTaint sentinel so the *import-time* nature propagates
// through import/re-export edges (a barrel importing this becomes
// side-effectful too).
affected = append(affected, "*", sideEffectTaint)
Comment on lines +243 to +247

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

Detect import-time side effects even when symbols also changed.

The enclosing fallback runs only when len(affected) == 0. If one diff changes an exported symbol and a top-level side-effect statement, this block does not run. The result omits sideEffectTaint, so importer and re-export propagation stops for that side-effect change.

Evaluate hasSideEffectStmtChanges and bareImportsChanged independently of symbol-level changes. Append "*" and sideEffectTaint whenever either check is true.

🤖 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/analyzer/astdiff.go` around lines 243 - 247, Update the
affected-symbol logic around hasSideEffectStmtChanges and bareImportsChanged so
these checks run independently of len(affected) and symbol-level changes.
Whenever either side-effect condition is true, append both "*" and
sideEffectTaint, while preserving existing handling for ordinary symbol changes.

for _, sym := range newAnalysis.Symbols {
if sym.IsTypeOnly && !includeTypes {
continue
Expand Down
Loading