Skip to content

Commit 5195541

Browse files
authored
fix: Detect runtime side-effect statements in entrypoint files
2 parents d1b632d + 44ba21e commit 5195541

4 files changed

Lines changed: 96 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.17.1] - 2026-04-10
9+
10+
### Fixed
11+
- Detect runtime side-effect statements (e.g. `console.log()`) in entrypoint/barrel files as affecting all exports — previously these were misclassified as "comments/imports only" and seeded zero taint
12+
813
## [0.17.0] - 2026-04-04
914

1015
### Changed
@@ -242,6 +247,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
242247
- Multi-stage Docker build
243248
- Automated vendor upgrade workflow
244249

250+
[0.17.1]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.17.0...v0.17.1
245251
[0.17.0]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.16.7...v0.17.0
246252
[0.16.7]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.16.6...v0.16.7
247253
[0.16.6]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.16.5...v0.16.6

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.17.0
1+
0.17.1

internal/analyzer/analyzer.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,11 +927,24 @@ func AnalyzeLibraryPackage(projectFolder string, entrypoints []Entrypoint, merge
927927
var affectedNames []string
928928
epDir := filepath.Dir(ep.SourceFile)
929929

930+
// If the entrypoint file itself has "*" taint (e.g. runtime side-effect
931+
// changes like console.log()), ALL exports are affected since every
932+
// importer will execute the entrypoint module at load time.
933+
epAllTainted := tainted[epStem]["*"]
934+
if epAllTainted {
935+
debugf(" entrypoint file has '*' taint — all exports affected")
936+
}
937+
930938
for _, exp := range epAnalysis.Exports {
931939
if exp.IsTypeOnly && !includeTypes {
932940
continue
933941
}
934942

943+
if epAllTainted {
944+
affectedNames = append(affectedNames, exp.Name)
945+
continue
946+
}
947+
935948
if exp.Source == "" {
936949
if tainted[epStem][exp.LocalName] || tainted[epStem]["*"] {
937950
affectedNames = append(affectedNames, exp.Name)

internal/analyzer/astdiff.go

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"goodchanges/internal/tsparse"
77
"goodchanges/tsgo-vendor/pkg/ast"
8+
"goodchanges/tsgo-vendor/pkg/scanner"
89
)
910

1011
// findAffectedSymbolsByASTDiff compares OLD and NEW file ASTs to find which symbols changed.
@@ -188,17 +189,30 @@ func findAffectedSymbolsByASTDiff(oldAnalysis *tsparse.FileAnalysis, newAnalysis
188189

189190
// Fallback: if no symbols were detected but the file clearly changed,
190191
// check if changes are outside any symbol (e.g. top-level side effects,
191-
// copyright comments). If there are exported symbols, taint them all.
192+
// copyright comments). If there are runtime side-effect changes, taint all symbols.
192193
if len(affected) == 0 && oldAnalysis != nil {
193194
oldText := ""
194195
if oldAnalysis.SourceFile != nil {
195196
oldText = oldAnalysis.SourceFile.Text()
196197
}
197198
if normalizeWhitespace(oldText) != normalizeWhitespace(newText) {
198199
// File changed but no symbol was affected — changes are outside symbols.
199-
// This could be comments, imports, or top-level side-effect code.
200-
// Don't taint anything — changes outside symbols don't affect exports.
201-
debugf(" file changed but no symbols affected (comments/imports only)")
200+
// Check if the changes include runtime side-effect statements.
201+
if hasSideEffectStmtChanges(oldAnalysis.SourceFile, newAnalysis.SourceFile) {
202+
debugf(" file changed with RUNTIME side-effect statements — tainting all symbols")
203+
// Use "*" wildcard to mark all exports as affected.
204+
// This handles barrel/entrypoint files that have no symbol declarations
205+
// but whose runtime side effects affect all importers.
206+
affected = append(affected, "*")
207+
for _, sym := range newAnalysis.Symbols {
208+
if sym.IsTypeOnly && !includeTypes {
209+
continue
210+
}
211+
affected = append(affected, sym.Name)
212+
}
213+
} else {
214+
debugf(" file changed but no symbols affected (comments/imports only)")
215+
}
202216
}
203217
}
204218

@@ -419,3 +433,61 @@ func normalizeWhitespace(s string) string {
419433
}
420434
return strings.TrimSpace(b.String())
421435
}
436+
437+
// hasSideEffectStmtChanges checks whether the top-level side-effect statements
438+
// (statements that are NOT declarations, imports, or exports) differ between
439+
// old and new source files. A change in side-effect statements means the module
440+
// has different runtime behavior at load time, affecting all importers.
441+
func hasSideEffectStmtChanges(oldSF *ast.SourceFile, newSF *ast.SourceFile) bool {
442+
oldText := collectSideEffectText(oldSF)
443+
newText := collectSideEffectText(newSF)
444+
return oldText != newText
445+
}
446+
447+
// collectSideEffectText extracts and normalizes the text of all top-level
448+
// side-effect statements from a source file. Side-effect statements are
449+
// everything except declarations, imports, exports, and empty statements.
450+
func collectSideEffectText(sf *ast.SourceFile) string {
451+
if sf == nil {
452+
return ""
453+
}
454+
sourceText := sf.Text()
455+
var b strings.Builder
456+
for _, stmt := range sf.Statements.Nodes {
457+
if isSideEffectStatement(stmt) {
458+
// Use SkipTrivia to exclude leading comments/whitespace so that
459+
// comment-only changes before a side-effect statement don't
460+
// cause false positives.
461+
start := scanner.SkipTrivia(sourceText, stmt.Pos())
462+
end := stmt.End()
463+
if start >= 0 && end <= len(sourceText) && start < end {
464+
b.WriteString(normalizeWhitespace(sourceText[start:end]))
465+
b.WriteByte('\n')
466+
}
467+
}
468+
}
469+
return b.String()
470+
}
471+
472+
// isSideEffectStatement returns true if a top-level statement is a runtime
473+
// side effect (not a declaration, import, export, or empty statement).
474+
// Examples: console.log(), Object.defineProperty(), bare function calls.
475+
func isSideEffectStatement(stmt *ast.Node) bool {
476+
switch stmt.Kind {
477+
case ast.KindFunctionDeclaration,
478+
ast.KindClassDeclaration,
479+
ast.KindInterfaceDeclaration,
480+
ast.KindTypeAliasDeclaration,
481+
ast.KindEnumDeclaration,
482+
ast.KindVariableStatement,
483+
ast.KindModuleDeclaration,
484+
ast.KindImportDeclaration,
485+
ast.KindImportEqualsDeclaration,
486+
ast.KindExportDeclaration,
487+
ast.KindExportAssignment,
488+
ast.KindEmptyStatement:
489+
return false
490+
default:
491+
return true
492+
}
493+
}

0 commit comments

Comments
 (0)