|
5 | 5 |
|
6 | 6 | "goodchanges/internal/tsparse" |
7 | 7 | "goodchanges/tsgo-vendor/pkg/ast" |
| 8 | + "goodchanges/tsgo-vendor/pkg/scanner" |
8 | 9 | ) |
9 | 10 |
|
10 | 11 | // findAffectedSymbolsByASTDiff compares OLD and NEW file ASTs to find which symbols changed. |
@@ -188,17 +189,30 @@ func findAffectedSymbolsByASTDiff(oldAnalysis *tsparse.FileAnalysis, newAnalysis |
188 | 189 |
|
189 | 190 | // Fallback: if no symbols were detected but the file clearly changed, |
190 | 191 | // 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. |
192 | 193 | if len(affected) == 0 && oldAnalysis != nil { |
193 | 194 | oldText := "" |
194 | 195 | if oldAnalysis.SourceFile != nil { |
195 | 196 | oldText = oldAnalysis.SourceFile.Text() |
196 | 197 | } |
197 | 198 | if normalizeWhitespace(oldText) != normalizeWhitespace(newText) { |
198 | 199 | // 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 | + } |
202 | 216 | } |
203 | 217 | } |
204 | 218 |
|
@@ -419,3 +433,61 @@ func normalizeWhitespace(s string) string { |
419 | 433 | } |
420 | 434 | return strings.TrimSpace(b.String()) |
421 | 435 | } |
| 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