sela-core 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -0
- package/bin/sela.js +3 -0
- package/dist/cli/ErrorHandler.d.ts +10 -0
- package/dist/cli/ErrorHandler.d.ts.map +1 -0
- package/dist/cli/ErrorHandler.js +70 -0
- package/dist/cli/commands/bulk.d.ts +3 -0
- package/dist/cli/commands/bulk.d.ts.map +1 -0
- package/dist/cli/commands/bulk.js +158 -0
- package/dist/cli/commands/find.d.ts +3 -0
- package/dist/cli/commands/find.d.ts.map +1 -0
- package/dist/cli/commands/find.js +51 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +166 -0
- package/dist/cli/commands/list.d.ts +3 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +56 -0
- package/dist/cli/commands/merge.d.ts +13 -0
- package/dist/cli/commands/merge.d.ts.map +1 -0
- package/dist/cli/commands/merge.js +119 -0
- package/dist/cli/commands/refactor.d.ts +3 -0
- package/dist/cli/commands/refactor.d.ts.map +1 -0
- package/dist/cli/commands/refactor.js +30 -0
- package/dist/cli/commands/showReport.d.ts +3 -0
- package/dist/cli/commands/showReport.d.ts.map +1 -0
- package/dist/cli/commands/showReport.js +82 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +51 -0
- package/dist/cli/commands/sync.d.ts +3 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +131 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +94 -0
- package/dist/cli/ui/DnaTable.d.ts +3 -0
- package/dist/cli/ui/DnaTable.d.ts.map +1 -0
- package/dist/cli/ui/DnaTable.js +77 -0
- package/dist/cli/ui/LocatorPicker.d.ts +8 -0
- package/dist/cli/ui/LocatorPicker.d.ts.map +1 -0
- package/dist/cli/ui/LocatorPicker.js +33 -0
- package/dist/cli/ui/ProgressReporter.d.ts +11 -0
- package/dist/cli/ui/ProgressReporter.d.ts.map +1 -0
- package/dist/cli/ui/ProgressReporter.js +33 -0
- package/dist/cli/ui/RefactorWizard.d.ts +26 -0
- package/dist/cli/ui/RefactorWizard.d.ts.map +1 -0
- package/dist/cli/ui/RefactorWizard.js +295 -0
- package/dist/config/ConfigLoader.d.ts +16 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -0
- package/dist/config/ConfigLoader.js +185 -0
- package/dist/config/DryRunGuard.d.ts +14 -0
- package/dist/config/DryRunGuard.d.ts.map +1 -0
- package/dist/config/DryRunGuard.js +80 -0
- package/dist/config/PathScopeGuard.d.ts +42 -0
- package/dist/config/PathScopeGuard.d.ts.map +1 -0
- package/dist/config/PathScopeGuard.js +192 -0
- package/dist/config/SelaConfig.d.ts +178 -0
- package/dist/config/SelaConfig.d.ts.map +1 -0
- package/dist/config/SelaConfig.js +141 -0
- package/dist/config/defineConfig.d.ts +3 -0
- package/dist/config/defineConfig.d.ts.map +1 -0
- package/dist/config/defineConfig.js +9 -0
- package/dist/engine/FixwrightEngine.d.ts +24 -0
- package/dist/engine/FixwrightEngine.d.ts.map +1 -0
- package/dist/engine/FixwrightEngine.js +403 -0
- package/dist/engine/HealingRegistry.d.ts +40 -0
- package/dist/engine/HealingRegistry.d.ts.map +1 -0
- package/dist/engine/HealingRegistry.js +99 -0
- package/dist/engine/SelaEngine.d.ts +57 -0
- package/dist/engine/SelaEngine.d.ts.map +1 -0
- package/dist/engine/SelaEngine.js +1146 -0
- package/dist/engine/singleton.d.ts +3 -0
- package/dist/engine/singleton.d.ts.map +1 -0
- package/dist/engine/singleton.js +5 -0
- package/dist/errors/SelaError.d.ts +146 -0
- package/dist/errors/SelaError.d.ts.map +1 -0
- package/dist/errors/SelaError.js +168 -0
- package/dist/fixtures/expectProxy.d.ts +12 -0
- package/dist/fixtures/expectProxy.d.ts.map +1 -0
- package/dist/fixtures/expectProxy.js +246 -0
- package/dist/fixtures/index.d.ts +30 -0
- package/dist/fixtures/index.d.ts.map +1 -0
- package/dist/fixtures/index.js +765 -0
- package/dist/fixtures/moduleExpect.d.ts +2 -0
- package/dist/fixtures/moduleExpect.d.ts.map +1 -0
- package/dist/fixtures/moduleExpect.js +54 -0
- package/dist/fixtures/proxyTag.d.ts +12 -0
- package/dist/fixtures/proxyTag.d.ts.map +1 -0
- package/dist/fixtures/proxyTag.js +45 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/reporter/SelaReporter.d.ts +165 -0
- package/dist/reporter/SelaReporter.d.ts.map +1 -0
- package/dist/reporter/SelaReporter.js +517 -0
- package/dist/services/ASTSourceUpdater.d.ts +143 -0
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -0
- package/dist/services/ASTSourceUpdater.js +3514 -0
- package/dist/services/AnchorResolver.d.ts +157 -0
- package/dist/services/AnchorResolver.d.ts.map +1 -0
- package/dist/services/AnchorResolver.js +289 -0
- package/dist/services/ArgumentTypeAnalyzer.d.ts +26 -0
- package/dist/services/ArgumentTypeAnalyzer.d.ts.map +1 -0
- package/dist/services/ArgumentTypeAnalyzer.js +92 -0
- package/dist/services/BlastRadiusAnalyzer.d.ts +15 -0
- package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -0
- package/dist/services/BlastRadiusAnalyzer.js +107 -0
- package/dist/services/ChainValidator.d.ts +76 -0
- package/dist/services/ChainValidator.d.ts.map +1 -0
- package/dist/services/ChainValidator.js +570 -0
- package/dist/services/CrossFileHealer.d.ts +6 -0
- package/dist/services/CrossFileHealer.d.ts.map +1 -0
- package/dist/services/CrossFileHealer.js +135 -0
- package/dist/services/DecisionEngine.d.ts +51 -0
- package/dist/services/DecisionEngine.d.ts.map +1 -0
- package/dist/services/DecisionEngine.js +260 -0
- package/dist/services/DefinitionTracer.d.ts +41 -0
- package/dist/services/DefinitionTracer.d.ts.map +1 -0
- package/dist/services/DefinitionTracer.js +366 -0
- package/dist/services/DnaEditorService.d.ts +31 -0
- package/dist/services/DnaEditorService.d.ts.map +1 -0
- package/dist/services/DnaEditorService.js +217 -0
- package/dist/services/DnaIndexService.d.ts +24 -0
- package/dist/services/DnaIndexService.d.ts.map +1 -0
- package/dist/services/DnaIndexService.js +131 -0
- package/dist/services/HealBudget.d.ts +32 -0
- package/dist/services/HealBudget.d.ts.map +1 -0
- package/dist/services/HealBudget.js +85 -0
- package/dist/services/HealReportService.d.ts +161 -0
- package/dist/services/HealReportService.d.ts.map +1 -0
- package/dist/services/HealReportService.js +1039 -0
- package/dist/services/HealingAdvisory.d.ts +30 -0
- package/dist/services/HealingAdvisory.d.ts.map +1 -0
- package/dist/services/HealingAdvisory.js +51 -0
- package/dist/services/HealingCacheService.d.ts +135 -0
- package/dist/services/HealingCacheService.d.ts.map +1 -0
- package/dist/services/HealingCacheService.js +404 -0
- package/dist/services/HealthReportService.d.ts +10 -0
- package/dist/services/HealthReportService.d.ts.map +1 -0
- package/dist/services/HealthReportService.js +84 -0
- package/dist/services/InitializerUpdater.d.ts +16 -0
- package/dist/services/InitializerUpdater.d.ts.map +1 -0
- package/dist/services/InitializerUpdater.js +73 -0
- package/dist/services/IntentAuditor.d.ts +56 -0
- package/dist/services/IntentAuditor.d.ts.map +1 -0
- package/dist/services/IntentAuditor.js +326 -0
- package/dist/services/InteractiveReview.d.ts +15 -0
- package/dist/services/InteractiveReview.d.ts.map +1 -0
- package/dist/services/InteractiveReview.js +128 -0
- package/dist/services/LLMService.d.ts +121 -0
- package/dist/services/LLMService.d.ts.map +1 -0
- package/dist/services/LLMService.js +475 -0
- package/dist/services/MutationApplier.d.ts +69 -0
- package/dist/services/MutationApplier.d.ts.map +1 -0
- package/dist/services/MutationApplier.js +360 -0
- package/dist/services/PRAutomationService.d.ts +50 -0
- package/dist/services/PRAutomationService.d.ts.map +1 -0
- package/dist/services/PRAutomationService.js +760 -0
- package/dist/services/PendingPromptLedger.d.ts +51 -0
- package/dist/services/PendingPromptLedger.d.ts.map +1 -0
- package/dist/services/PendingPromptLedger.js +205 -0
- package/dist/services/ReportGenerator.d.ts +156 -0
- package/dist/services/ReportGenerator.d.ts.map +1 -0
- package/dist/services/ReportGenerator.js +278 -0
- package/dist/services/ReportMergeService.d.ts +95 -0
- package/dist/services/ReportMergeService.d.ts.map +1 -0
- package/dist/services/ReportMergeService.js +0 -0
- package/dist/services/SafetyGuard.d.ts +112 -0
- package/dist/services/SafetyGuard.d.ts.map +1 -0
- package/dist/services/SafetyGuard.js +87 -0
- package/dist/services/Sanitizer.d.ts +28 -0
- package/dist/services/Sanitizer.d.ts.map +1 -0
- package/dist/services/Sanitizer.js +142 -0
- package/dist/services/SanitizingAnthropic.d.ts +19 -0
- package/dist/services/SanitizingAnthropic.d.ts.map +1 -0
- package/dist/services/SanitizingAnthropic.js +84 -0
- package/dist/services/SelectorSanitizer.d.ts +52 -0
- package/dist/services/SelectorSanitizer.d.ts.map +1 -0
- package/dist/services/SelectorSanitizer.js +318 -0
- package/dist/services/SnapshotService.d.ts +11 -0
- package/dist/services/SnapshotService.d.ts.map +1 -0
- package/dist/services/SnapshotService.js +352 -0
- package/dist/services/SourceLinkService.d.ts +26 -0
- package/dist/services/SourceLinkService.d.ts.map +1 -0
- package/dist/services/SourceLinkService.js +165 -0
- package/dist/services/SourceUpdater.d.ts +45 -0
- package/dist/services/SourceUpdater.d.ts.map +1 -0
- package/dist/services/SourceUpdater.js +1091 -0
- package/dist/services/TemplateDiffService.d.ts +25 -0
- package/dist/services/TemplateDiffService.d.ts.map +1 -0
- package/dist/services/TemplateDiffService.js +334 -0
- package/dist/services/TraceBackEngine.d.ts +67 -0
- package/dist/services/TraceBackEngine.d.ts.map +1 -0
- package/dist/services/TraceBackEngine.js +672 -0
- package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
- package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
- package/dist/services/WorkspaceSnapshotService.js +200 -0
- package/dist/services/safety/PipelineContext.d.ts +63 -0
- package/dist/services/safety/PipelineContext.d.ts.map +1 -0
- package/dist/services/safety/PipelineContext.js +34 -0
- package/dist/services/safety/roleClassifier.d.ts +4 -0
- package/dist/services/safety/roleClassifier.d.ts.map +1 -0
- package/dist/services/safety/roleClassifier.js +233 -0
- package/dist/services/safety/rules/ConfidenceTierRule.d.ts +3 -0
- package/dist/services/safety/rules/ConfidenceTierRule.d.ts.map +1 -0
- package/dist/services/safety/rules/ConfidenceTierRule.js +32 -0
- package/dist/services/safety/rules/DangerousSinkRule.d.ts +10 -0
- package/dist/services/safety/rules/DangerousSinkRule.d.ts.map +1 -0
- package/dist/services/safety/rules/DangerousSinkRule.js +53 -0
- package/dist/services/safety/rules/FunctionalRoleRule.d.ts +3 -0
- package/dist/services/safety/rules/FunctionalRoleRule.d.ts.map +1 -0
- package/dist/services/safety/rules/FunctionalRoleRule.js +64 -0
- package/dist/services/safety/rules/HardConfidenceRule.d.ts +3 -0
- package/dist/services/safety/rules/HardConfidenceRule.d.ts.map +1 -0
- package/dist/services/safety/rules/HardConfidenceRule.js +22 -0
- package/dist/services/safety/rules/IntentAuditorRule.d.ts +3 -0
- package/dist/services/safety/rules/IntentAuditorRule.d.ts.map +1 -0
- package/dist/services/safety/rules/IntentAuditorRule.js +97 -0
- package/dist/services/safety/rules/SemanticOppositeRule.d.ts +3 -0
- package/dist/services/safety/rules/SemanticOppositeRule.d.ts.map +1 -0
- package/dist/services/safety/rules/SemanticOppositeRule.js +30 -0
- package/dist/services/safety/rules/StructuralOnlyRule.d.ts +3 -0
- package/dist/services/safety/rules/StructuralOnlyRule.d.ts.map +1 -0
- package/dist/services/safety/rules/StructuralOnlyRule.js +38 -0
- package/dist/services/safety/rules/VisibilityRule.d.ts +3 -0
- package/dist/services/safety/rules/VisibilityRule.d.ts.map +1 -0
- package/dist/services/safety/rules/VisibilityRule.js +53 -0
- package/dist/services/safety/rules/ZeroTrustRule.d.ts +3 -0
- package/dist/services/safety/rules/ZeroTrustRule.d.ts.map +1 -0
- package/dist/services/safety/rules/ZeroTrustRule.js +37 -0
- package/dist/services/safety/rules/index.d.ts +3 -0
- package/dist/services/safety/rules/index.d.ts.map +1 -0
- package/dist/services/safety/rules/index.js +34 -0
- package/dist/services/types.d.ts +59 -0
- package/dist/services/types.d.ts.map +1 -0
- package/dist/services/types.js +2 -0
- package/dist/storage/SnapshotManager.d.ts +5 -0
- package/dist/storage/SnapshotManager.d.ts.map +1 -0
- package/dist/storage/SnapshotManager.js +32 -0
- package/dist/types/index.d.ts +95 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/utils/DOMUtils.d.ts +49 -0
- package/dist/utils/DOMUtils.d.ts.map +1 -0
- package/dist/utils/DOMUtils.js +466 -0
- package/dist/utils/FileLock.d.ts +44 -0
- package/dist/utils/FileLock.d.ts.map +1 -0
- package/dist/utils/FileLock.js +189 -0
- package/dist/utils/IsolatedDiff.d.ts +45 -0
- package/dist/utils/IsolatedDiff.d.ts.map +1 -0
- package/dist/utils/IsolatedDiff.js +382 -0
- package/dist/utils/StackUtils.d.ts +11 -0
- package/dist/utils/StackUtils.d.ts.map +1 -0
- package/dist/utils/StackUtils.js +120 -0
- package/dist/utils/SummaryTable.d.ts +17 -0
- package/dist/utils/SummaryTable.d.ts.map +1 -0
- package/dist/utils/SummaryTable.js +37 -0
- package/dist/utils/atomicWrite.d.ts +9 -0
- package/dist/utils/atomicWrite.d.ts.map +1 -0
- package/dist/utils/atomicWrite.js +83 -0
- package/dist/utils/logger.d.ts +59 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +123 -0
- package/dist/vendor/enquirer.d.ts +33 -0
- package/dist/vendor/enquirer.d.ts.map +1 -0
- package/dist/vendor/enquirer.js +11 -0
- package/package.json +71 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/services/TraceBackEngine.ts
|
|
3
|
+
//
|
|
4
|
+
// Ticket #2 — Stage 2: The AST Trace-Back Engine.
|
|
5
|
+
//
|
|
6
|
+
// Given a node at (or under) a failing call-site — e.g. a token inside
|
|
7
|
+
// `await targetButton.nth(3).click()` — climb the PropertyAccess / Call
|
|
8
|
+
// spine to the leftmost receiver, resolve that receiver's DEFINITION, and
|
|
9
|
+
// classify it:
|
|
10
|
+
//
|
|
11
|
+
// • VariableDeclaration / PropertyDeclaration (POM) → mutation TARGET
|
|
12
|
+
// • Parameter / ArrowFunction / FunctionDeclaration → factory → ABORT(hard)
|
|
13
|
+
// • TemplateExpression w/ substitution, ?? / || / ?: init → ABORT(hard)
|
|
14
|
+
// • destructuring binding, reassigned let/var → ABORT(hard)
|
|
15
|
+
// • imported / unresolved symbol → ABORT(soft, cross-file)
|
|
16
|
+
//
|
|
17
|
+
// Playwright semantics handled natively: locator / getByRole / getByText /
|
|
18
|
+
// getByTestId / …, frameLocator (iframes), `.parent.locator('.child')` and
|
|
19
|
+
// `'.parent >> .child'` chaining (shadow DOM), action modifiers
|
|
20
|
+
// (.nth/.first/.last/.filter), and awaited execution (await/parens unwrap).
|
|
21
|
+
//
|
|
22
|
+
// The engine is PURE and never writes. It composes with Stage 1: every
|
|
23
|
+
// successful target carries an AnchorKey (computeAnchor) so the drain loop
|
|
24
|
+
// can relocate it later without line numbers.
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.TraceBackEngine = void 0;
|
|
27
|
+
exports.categorizeMethod = categorizeMethod;
|
|
28
|
+
exports.climbToTopCall = climbToTopCall;
|
|
29
|
+
exports.getLeftmostReceiver = getLeftmostReceiver;
|
|
30
|
+
exports.resolveValueDeclaration = resolveValueDeclaration;
|
|
31
|
+
exports.stripTrailingActions = stripTrailingActions;
|
|
32
|
+
const ts_morph_1 = require("ts-morph");
|
|
33
|
+
const AnchorResolver_1 = require("./AnchorResolver");
|
|
34
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
35
|
+
// PLAYWRIGHT METHOD TAXONOMY
|
|
36
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
37
|
+
const SELECTOR_METHODS = new Set([
|
|
38
|
+
"locator",
|
|
39
|
+
"getByRole",
|
|
40
|
+
"getByText",
|
|
41
|
+
"getByLabel",
|
|
42
|
+
"getByPlaceholder",
|
|
43
|
+
"getByTestId",
|
|
44
|
+
"getByAltText",
|
|
45
|
+
"getByTitle",
|
|
46
|
+
]);
|
|
47
|
+
/** Iframe / frame traversal — must be climbed through seamlessly. */
|
|
48
|
+
const FRAME_METHODS = new Set(["frameLocator", "contentFrame"]);
|
|
49
|
+
/** Sub-selection that does NOT change the locator's identity root. */
|
|
50
|
+
const MODIFIER_METHODS = new Set([
|
|
51
|
+
"nth",
|
|
52
|
+
"first",
|
|
53
|
+
"last",
|
|
54
|
+
"filter",
|
|
55
|
+
"and",
|
|
56
|
+
"or",
|
|
57
|
+
]);
|
|
58
|
+
/** Terminal actions / queries — strippable trailing calls. */
|
|
59
|
+
const ACTION_METHODS = new Set([
|
|
60
|
+
"click",
|
|
61
|
+
"dblclick",
|
|
62
|
+
"fill",
|
|
63
|
+
"type",
|
|
64
|
+
"pressSequentially",
|
|
65
|
+
"press",
|
|
66
|
+
"check",
|
|
67
|
+
"uncheck",
|
|
68
|
+
"setChecked",
|
|
69
|
+
"hover",
|
|
70
|
+
"focus",
|
|
71
|
+
"blur",
|
|
72
|
+
"tap",
|
|
73
|
+
"selectOption",
|
|
74
|
+
"selectText",
|
|
75
|
+
"setInputFiles",
|
|
76
|
+
"clear",
|
|
77
|
+
"dragTo",
|
|
78
|
+
"screenshot",
|
|
79
|
+
"scrollIntoViewIfNeeded",
|
|
80
|
+
"waitFor",
|
|
81
|
+
"isVisible",
|
|
82
|
+
"isHidden",
|
|
83
|
+
"isEnabled",
|
|
84
|
+
"isDisabled",
|
|
85
|
+
"isChecked",
|
|
86
|
+
"isEditable",
|
|
87
|
+
"textContent",
|
|
88
|
+
"innerText",
|
|
89
|
+
"innerHTML",
|
|
90
|
+
"inputValue",
|
|
91
|
+
"getAttribute",
|
|
92
|
+
"count",
|
|
93
|
+
"all",
|
|
94
|
+
"allInnerTexts",
|
|
95
|
+
"allTextContents",
|
|
96
|
+
"boundingBox",
|
|
97
|
+
"evaluate",
|
|
98
|
+
"evaluateHandle",
|
|
99
|
+
"elementHandle",
|
|
100
|
+
"highlight",
|
|
101
|
+
"dispatchEvent",
|
|
102
|
+
]);
|
|
103
|
+
/** Stable chain roots that are NOT user variables (no trace-back needed). */
|
|
104
|
+
const PAGE_ROOT_NAMES = new Set([
|
|
105
|
+
"page",
|
|
106
|
+
"context",
|
|
107
|
+
"browser",
|
|
108
|
+
"playwright",
|
|
109
|
+
"request",
|
|
110
|
+
"frame",
|
|
111
|
+
]);
|
|
112
|
+
function categorizeMethod(name) {
|
|
113
|
+
if (FRAME_METHODS.has(name))
|
|
114
|
+
return "frame";
|
|
115
|
+
if (SELECTOR_METHODS.has(name))
|
|
116
|
+
return "selector";
|
|
117
|
+
if (MODIFIER_METHODS.has(name))
|
|
118
|
+
return "modifier";
|
|
119
|
+
if (ACTION_METHODS.has(name))
|
|
120
|
+
return "action";
|
|
121
|
+
return "unknown";
|
|
122
|
+
}
|
|
123
|
+
const MAX_DEPTH = 12;
|
|
124
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
125
|
+
// ENTRY POINTS
|
|
126
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
127
|
+
class TraceBackEngine {
|
|
128
|
+
/** Phase 0 convenience: resolve (1-based line, 0-based col) → node → trace. */
|
|
129
|
+
static traceFromPosition(sourceFile, line, column) {
|
|
130
|
+
let pos;
|
|
131
|
+
try {
|
|
132
|
+
pos = sourceFile.compilerNode.getPositionOfLineAndCharacter(line - 1, column);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return abort("hard", "DEFINITION_NOT_FOUND", `bad position ${line}:${column}`);
|
|
136
|
+
}
|
|
137
|
+
const node = sourceFile.getDescendantAtPos(pos);
|
|
138
|
+
if (!node) {
|
|
139
|
+
return abort("hard", "DEFINITION_NOT_FOUND", `no node at ${line}:${column}`);
|
|
140
|
+
}
|
|
141
|
+
return TraceBackEngine.traceFromNode(node);
|
|
142
|
+
}
|
|
143
|
+
/** Phases 1–5. */
|
|
144
|
+
static traceFromNode(start) {
|
|
145
|
+
// ── Phase 1: climb to the top-level call of the failing statement ──
|
|
146
|
+
const topCall = climbToTopCall(start);
|
|
147
|
+
if (!topCall) {
|
|
148
|
+
return abort("hard", "UNSUPPORTED", `no enclosing call expression for ${start.getKindName()}`);
|
|
149
|
+
}
|
|
150
|
+
// ── Phase 2: descend the spine → leftmost receiver + chain ──────────
|
|
151
|
+
const { leftmost, chain } = getLeftmostReceiver(topCall);
|
|
152
|
+
// Case C/D: receiver is a property access (POM `this.x` / `obj.x`).
|
|
153
|
+
if (ts_morph_1.Node.isPropertyAccessExpression(leftmost)) {
|
|
154
|
+
return tracePropertyReceiver(leftmost, chain, topCall);
|
|
155
|
+
}
|
|
156
|
+
// Case A: receiver is a page-root identifier → inline or var-initializer.
|
|
157
|
+
if (ts_morph_1.Node.isIdentifier(leftmost) && PAGE_ROOT_NAMES.has(leftmost.getText())) {
|
|
158
|
+
return traceRootedChain(leftmost.getText(), topCall, chain);
|
|
159
|
+
}
|
|
160
|
+
// Case B: receiver is a local variable → resolve + classify.
|
|
161
|
+
if (ts_morph_1.Node.isIdentifier(leftmost)) {
|
|
162
|
+
return traceVariableReceiver(leftmost, chain, 0);
|
|
163
|
+
}
|
|
164
|
+
return abort("hard", "UNSUPPORTED", `leftmost receiver is ${leftmost.getKindName()}`, leftmost);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
exports.TraceBackEngine = TraceBackEngine;
|
|
168
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
169
|
+
// PHASE 1 — climb to the statement's top call
|
|
170
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
171
|
+
function climbToTopCall(start) {
|
|
172
|
+
let n = start;
|
|
173
|
+
let top = null;
|
|
174
|
+
let enclosingStmt;
|
|
175
|
+
while (n) {
|
|
176
|
+
if (ts_morph_1.Node.isCallExpression(n))
|
|
177
|
+
top = n;
|
|
178
|
+
if (ts_morph_1.Node.isStatement(n))
|
|
179
|
+
enclosingStmt = n;
|
|
180
|
+
const p = n.getParent();
|
|
181
|
+
if (!p)
|
|
182
|
+
break;
|
|
183
|
+
// Stop at the statement/scope boundary — we want the OUTERMOST call of
|
|
184
|
+
// this expression, not a call further up the tree.
|
|
185
|
+
if (ts_morph_1.Node.isBlock(p) || ts_morph_1.Node.isSourceFile(p))
|
|
186
|
+
break;
|
|
187
|
+
n = p;
|
|
188
|
+
}
|
|
189
|
+
if (top)
|
|
190
|
+
return top;
|
|
191
|
+
// Fallback: `start` was a SIBLING of the call, not an ancestor — e.g. the
|
|
192
|
+
// `await` keyword in `await btn.click()` (climbing never reaches the
|
|
193
|
+
// CallExpression, which is a CHILD of the AwaitExpression). Descend the
|
|
194
|
+
// enclosing statement and return its OUTERMOST call (largest end offset =
|
|
195
|
+
// the last-closing call in a chain like `btn.nth(3).click()`).
|
|
196
|
+
const scope = enclosingStmt ?? start;
|
|
197
|
+
const calls = scope.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression);
|
|
198
|
+
if (calls.length === 0)
|
|
199
|
+
return null;
|
|
200
|
+
let best = calls[0];
|
|
201
|
+
for (const c of calls) {
|
|
202
|
+
if (c.getEnd() > best.getEnd())
|
|
203
|
+
best = c;
|
|
204
|
+
}
|
|
205
|
+
return best;
|
|
206
|
+
}
|
|
207
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
208
|
+
// PHASE 2 — leftmost receiver + chain extraction
|
|
209
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
210
|
+
function getLeftmostReceiver(topCall) {
|
|
211
|
+
const chain = [];
|
|
212
|
+
let expr = topCall;
|
|
213
|
+
// Bound the descent so a pathological tree can't loop forever.
|
|
214
|
+
for (let i = 0; i < 256; i++) {
|
|
215
|
+
expr = unwrap(expr);
|
|
216
|
+
if (ts_morph_1.Node.isCallExpression(expr)) {
|
|
217
|
+
const callee = unwrap(expr.getExpression());
|
|
218
|
+
if (ts_morph_1.Node.isPropertyAccessExpression(callee)) {
|
|
219
|
+
chain.push({
|
|
220
|
+
name: callee.getName(),
|
|
221
|
+
category: categorizeMethod(callee.getName()),
|
|
222
|
+
});
|
|
223
|
+
expr = callee.getExpression();
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
// Call on a non-property (e.g. a factory invocation getRow()) — descend.
|
|
227
|
+
expr = callee;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
|
|
231
|
+
// Property access NOT followed by a call at the leftmost position:
|
|
232
|
+
// `this.submitBtn` / `loginPage.submitBtn`. The base is the receiver.
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
chain.reverse();
|
|
238
|
+
return { leftmost: expr, chain };
|
|
239
|
+
}
|
|
240
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
241
|
+
// CASE A — chain rooted directly at page / context / …
|
|
242
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
243
|
+
function traceRootedChain(rootReceiver, topCall, chain) {
|
|
244
|
+
const locatorChain = stripTrailingActions(topCall);
|
|
245
|
+
// Dynamic-selector guard: page.locator(`#row-${id}`) is not safely
|
|
246
|
+
// mutable at source.
|
|
247
|
+
if (containsDynamicSelectorArg(locatorChain)) {
|
|
248
|
+
return abort("hard", "DYNAMIC_SELECTOR", "selector built from a template literal with substitutions", locatorChain);
|
|
249
|
+
}
|
|
250
|
+
// If the chain is the initializer of a variable, the real target is that
|
|
251
|
+
// declaration (start happened to land inside the declaration). Otherwise
|
|
252
|
+
// it is a bare inline call-site.
|
|
253
|
+
const enclosingDecl = getEnclosingVariableDeclaration(locatorChain);
|
|
254
|
+
if (enclosingDecl &&
|
|
255
|
+
enclosingDecl.getInitializer() &&
|
|
256
|
+
isWithin(locatorChain, enclosingDecl.getInitializer())) {
|
|
257
|
+
const target = makeTarget("VariableDeclaration", enclosingDecl.getInitializer(), enclosingDecl, rootReceiver);
|
|
258
|
+
return { status: "target", target, callSiteChain: chain, dependencyChain: [target] };
|
|
259
|
+
}
|
|
260
|
+
const target = makeTarget("InlineCallSite", locatorChain, null, rootReceiver);
|
|
261
|
+
return { status: "target", target, callSiteChain: chain, dependencyChain: [] };
|
|
262
|
+
}
|
|
263
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
264
|
+
// CASE B — local variable receiver: resolve + classify (Phase 3–5)
|
|
265
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
266
|
+
function traceVariableReceiver(idNode, chain, depth) {
|
|
267
|
+
if (depth > MAX_DEPTH) {
|
|
268
|
+
return abort("hard", "MAX_DEPTH", "alias chain exceeded max depth");
|
|
269
|
+
}
|
|
270
|
+
const name = idNode.getText();
|
|
271
|
+
const sourceFile = idNode.getSourceFile();
|
|
272
|
+
const useOffset = idNode.getStart();
|
|
273
|
+
// Phase 3: resolve the definition in lexical scope.
|
|
274
|
+
const decl = resolveValueDeclaration(idNode, name, useOffset);
|
|
275
|
+
if (!decl) {
|
|
276
|
+
// Imported / unresolved → cross-file (soft) vs truly missing.
|
|
277
|
+
if (isImported(sourceFile, name)) {
|
|
278
|
+
return abort("soft", "CROSS_FILE", `"${name}" is imported`, idNode);
|
|
279
|
+
}
|
|
280
|
+
if (findDestructuringBinding(sourceFile, name, useOffset)) {
|
|
281
|
+
return abort("hard", "DESTRUCTURED", `"${name}" comes from a destructuring binding`);
|
|
282
|
+
}
|
|
283
|
+
return abort("hard", "DEFINITION_NOT_FOUND", `"${name}" not resolved in scope`, idNode);
|
|
284
|
+
}
|
|
285
|
+
// Phase 4: classify the declaration kind.
|
|
286
|
+
if (ts_morph_1.Node.isParameterDeclaration(decl)) {
|
|
287
|
+
return abort("hard", "PARAMETER", `"${name}" is a function parameter`, decl);
|
|
288
|
+
}
|
|
289
|
+
if (ts_morph_1.Node.isFunctionDeclaration(decl)) {
|
|
290
|
+
return abort("hard", "FACTORY", `"${name}" is a function declaration`, decl);
|
|
291
|
+
}
|
|
292
|
+
if (!ts_morph_1.Node.isVariableDeclaration(decl)) {
|
|
293
|
+
return abort("hard", "UNSUPPORTED", `"${name}" resolves to ${decl.getKindName()}`, decl);
|
|
294
|
+
}
|
|
295
|
+
return classifyVariableDeclaration(decl, chain, depth);
|
|
296
|
+
}
|
|
297
|
+
function classifyVariableDeclaration(decl, chain, depth) {
|
|
298
|
+
const name = decl.getName();
|
|
299
|
+
const init = decl.getInitializer();
|
|
300
|
+
if (!init) {
|
|
301
|
+
// No initializer. The standard Playwright fixture pattern is
|
|
302
|
+
// let submit; test.beforeEach(() => { submit = page.locator('.btn'); });
|
|
303
|
+
// A SINGLE standalone assignment is the de-facto definition — resolve it.
|
|
304
|
+
// Multiple assignments = genuinely flow-sensitive → abort.
|
|
305
|
+
const assigns = findStandaloneAssignments(decl);
|
|
306
|
+
if (assigns.length === 0) {
|
|
307
|
+
return abort("hard", "UNSUPPORTED", `"${name}" has no initializer`, decl);
|
|
308
|
+
}
|
|
309
|
+
if (assigns.length > 1) {
|
|
310
|
+
return abort("hard", "REASSIGNED", `"${name}" is assigned in ${assigns.length} places`, decl);
|
|
311
|
+
}
|
|
312
|
+
return classifyInitializerExpr(assigns[0], decl, chain, depth, name, true);
|
|
313
|
+
}
|
|
314
|
+
// Has an initializer AND a later standalone assignment → reassigned.
|
|
315
|
+
if (isReassigned(decl)) {
|
|
316
|
+
return abort("hard", "REASSIGNED", `"${name}" is reassigned after init`, decl);
|
|
317
|
+
}
|
|
318
|
+
return classifyInitializerExpr(init, decl, chain, depth, name, false);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Classify the value expression bound to a variable (its initializer, or the
|
|
322
|
+
* RHS of its single hook-style assignment) and build the target / cascade.
|
|
323
|
+
*/
|
|
324
|
+
function classifyInitializerExpr(expr, decl, chain, depth, name, fromAssignment) {
|
|
325
|
+
// Factory: const f = (…) => … / function expression.
|
|
326
|
+
if (ts_morph_1.Node.isArrowFunction(expr) || ts_morph_1.Node.isFunctionExpression(expr)) {
|
|
327
|
+
return abort("hard", "FACTORY", `"${name}" is a factory function`, decl);
|
|
328
|
+
}
|
|
329
|
+
// Branchy init: cond ? a : b / a ?? b / a || b.
|
|
330
|
+
if (ts_morph_1.Node.isConditionalExpression(expr) ||
|
|
331
|
+
(ts_morph_1.Node.isBinaryExpression(expr) && isNullishOrLogical(expr))) {
|
|
332
|
+
return abort("hard", "DYNAMIC_SELECTOR", `"${name}" has a conditional / branchy initializer`, decl);
|
|
333
|
+
}
|
|
334
|
+
// Alias: const a = b → recurse on b.
|
|
335
|
+
if (ts_morph_1.Node.isIdentifier(expr)) {
|
|
336
|
+
return traceVariableReceiver(expr, chain, depth + 1);
|
|
337
|
+
}
|
|
338
|
+
if (ts_morph_1.Node.isCallExpression(expr)) {
|
|
339
|
+
if (containsDynamicSelectorArg(expr)) {
|
|
340
|
+
return abort("hard", "DYNAMIC_SELECTOR", `"${name}" selector uses a template literal with substitutions`, decl);
|
|
341
|
+
}
|
|
342
|
+
if (!isPlaywrightChain(expr)) {
|
|
343
|
+
return abort("hard", "UNSUPPORTED", `"${name}" initializer is not a Playwright locator chain`, decl);
|
|
344
|
+
}
|
|
345
|
+
const { rootReceiver, receiverVar } = analyzeChainRoot(expr);
|
|
346
|
+
// The mutation target is the value expression; the owning declaration is
|
|
347
|
+
// the `let`/`const` so the reference census keys off the variable name.
|
|
348
|
+
const target = makeTarget("VariableDeclaration", expr, decl, rootReceiver, fromAssignment ? name : undefined);
|
|
349
|
+
let dependencyChain = [target];
|
|
350
|
+
if (receiverVar) {
|
|
351
|
+
const upstream = traceVariableReceiver(receiverVar, [], depth + 1);
|
|
352
|
+
if (upstream.status === "target") {
|
|
353
|
+
dependencyChain = [...upstream.dependencyChain, target];
|
|
354
|
+
target.rootReceiver = upstream.target.rootReceiver;
|
|
355
|
+
}
|
|
356
|
+
// Upstream abort → keep THIS decl as a valid leaf target.
|
|
357
|
+
}
|
|
358
|
+
return { status: "target", target, callSiteChain: chain, dependencyChain };
|
|
359
|
+
}
|
|
360
|
+
return abort("hard", "UNSUPPORTED", `"${name}" initializer is ${expr.getKindName()}`, decl);
|
|
361
|
+
}
|
|
362
|
+
/** Standalone `name = …` assignments in the file (excludes the decl init). */
|
|
363
|
+
function findStandaloneAssignments(decl) {
|
|
364
|
+
const name = decl.getName();
|
|
365
|
+
const sf = decl.getSourceFile();
|
|
366
|
+
const out = [];
|
|
367
|
+
for (const bin of sf.getDescendantsOfKind(ts_morph_1.SyntaxKind.BinaryExpression)) {
|
|
368
|
+
if (bin.getOperatorToken().getKind() !== ts_morph_1.SyntaxKind.EqualsToken)
|
|
369
|
+
continue;
|
|
370
|
+
const left = bin.getLeft();
|
|
371
|
+
if (ts_morph_1.Node.isIdentifier(left) && left.getText() === name) {
|
|
372
|
+
out.push(bin.getRight());
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
378
|
+
// CASE C/D — property-access receiver (POM)
|
|
379
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
380
|
+
function tracePropertyReceiver(prop, chain, _topCall) {
|
|
381
|
+
const base = unwrap(prop.getExpression());
|
|
382
|
+
const propName = prop.getName();
|
|
383
|
+
// C: this.submitBtn → resolve same-file class member.
|
|
384
|
+
if (base.getKind() === ts_morph_1.SyntaxKind.ThisKeyword) {
|
|
385
|
+
const cls = prop.getFirstAncestorByKind(ts_morph_1.SyntaxKind.ClassDeclaration);
|
|
386
|
+
if (!cls) {
|
|
387
|
+
return abort("hard", "DEFINITION_NOT_FOUND", `this.${propName} has no enclosing class`);
|
|
388
|
+
}
|
|
389
|
+
// Property with an initializer: btn = page.locator('#go').
|
|
390
|
+
const propertyDecl = cls.getProperty(propName);
|
|
391
|
+
if (propertyDecl && ts_morph_1.Node.isPropertyDeclaration(propertyDecl)) {
|
|
392
|
+
const init = propertyDecl.getInitializer();
|
|
393
|
+
if (init && ts_morph_1.Node.isCallExpression(init)) {
|
|
394
|
+
if (containsDynamicSelectorArg(init)) {
|
|
395
|
+
return abort("hard", "DYNAMIC_SELECTOR", `this.${propName} uses a dynamic selector`, propertyDecl);
|
|
396
|
+
}
|
|
397
|
+
if (isPlaywrightChain(init)) {
|
|
398
|
+
const { rootReceiver } = analyzeChainRoot(init);
|
|
399
|
+
const target = makeTarget("PropertyDeclaration", init, propertyDecl, rootReceiver);
|
|
400
|
+
return {
|
|
401
|
+
status: "target",
|
|
402
|
+
target,
|
|
403
|
+
callSiteChain: chain,
|
|
404
|
+
dependencyChain: [target],
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Constructor assignment: this.submitBtn = page.locator('#submit').
|
|
410
|
+
const assignRhs = findConstructorAssignment(cls, propName);
|
|
411
|
+
if (assignRhs && ts_morph_1.Node.isCallExpression(assignRhs)) {
|
|
412
|
+
if (containsDynamicSelectorArg(assignRhs)) {
|
|
413
|
+
return abort("hard", "DYNAMIC_SELECTOR", `this.${propName} uses a dynamic selector`, assignRhs);
|
|
414
|
+
}
|
|
415
|
+
if (isPlaywrightChain(assignRhs)) {
|
|
416
|
+
const { rootReceiver } = analyzeChainRoot(assignRhs);
|
|
417
|
+
const target = makeTarget("PropertyAssignment", assignRhs, assignRhs.getParent() ?? null, rootReceiver);
|
|
418
|
+
return {
|
|
419
|
+
status: "target",
|
|
420
|
+
target,
|
|
421
|
+
callSiteChain: chain,
|
|
422
|
+
dependencyChain: [target],
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return abort("hard", "DEFINITION_NOT_FOUND", `this.${propName} not found as a locator on the class`);
|
|
427
|
+
}
|
|
428
|
+
// D: obj.submitBtn where obj is a local var (often `new SomeClass()`).
|
|
429
|
+
// Same-file class resolution is out of Stage-2 scope; if obj is imported
|
|
430
|
+
// or its class is imported, this is a cross-file POM (soft abort).
|
|
431
|
+
if (ts_morph_1.Node.isIdentifier(base)) {
|
|
432
|
+
return abort("soft", "CROSS_FILE", `"${base.getText()}.${propName}" is a page-object reference (cross-file resolution required)`, prop);
|
|
433
|
+
}
|
|
434
|
+
return abort("hard", "UNSUPPORTED", `property receiver base is ${base.getKindName()}`, prop);
|
|
435
|
+
}
|
|
436
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
437
|
+
// SCOPE-AWARE DEFINITION RESOLUTION
|
|
438
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
439
|
+
/**
|
|
440
|
+
* Lexical-scope resolution of `name` as seen from `useOffset`. Walks
|
|
441
|
+
* enclosing scopes outward: parameters of enclosing functions, then
|
|
442
|
+
* variable/function declarations introduced in each enclosing block /
|
|
443
|
+
* source file. Pure compiler-API traversal — no language service.
|
|
444
|
+
*/
|
|
445
|
+
function resolveValueDeclaration(fromNode, name, useOffset) {
|
|
446
|
+
let cur = fromNode.getParent();
|
|
447
|
+
while (cur) {
|
|
448
|
+
if (isFunctionLike(cur)) {
|
|
449
|
+
const params = cur.getParameters?.();
|
|
450
|
+
const param = params?.find((p) => p.getName() === name);
|
|
451
|
+
if (param)
|
|
452
|
+
return param;
|
|
453
|
+
}
|
|
454
|
+
if (ts_morph_1.Node.isBlock(cur) || ts_morph_1.Node.isSourceFile(cur)) {
|
|
455
|
+
const found = scanScopeForDecl(cur, name, useOffset);
|
|
456
|
+
if (found)
|
|
457
|
+
return found;
|
|
458
|
+
}
|
|
459
|
+
cur = cur.getParent();
|
|
460
|
+
}
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
function scanScopeForDecl(scope, name, beforeOffset) {
|
|
464
|
+
// getStatements() returns the scope's direct statements (it descends the
|
|
465
|
+
// synthetic SyntaxList that getChildrenOfKind would stop at).
|
|
466
|
+
const stmts = scope.getStatements?.();
|
|
467
|
+
if (!stmts)
|
|
468
|
+
return null;
|
|
469
|
+
// Function declarations are hoisted — match regardless of position.
|
|
470
|
+
for (const stmt of stmts) {
|
|
471
|
+
if (ts_morph_1.Node.isFunctionDeclaration(stmt) && stmt.getName() === name) {
|
|
472
|
+
return stmt;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// Variable declarations: closest one declared before the use site.
|
|
476
|
+
let best = null;
|
|
477
|
+
let bestStart = -1;
|
|
478
|
+
for (const stmt of stmts) {
|
|
479
|
+
if (!ts_morph_1.Node.isVariableStatement(stmt))
|
|
480
|
+
continue;
|
|
481
|
+
for (const d of stmt.getDeclarations()) {
|
|
482
|
+
if (d.getName() !== name)
|
|
483
|
+
continue;
|
|
484
|
+
const s = d.getStart();
|
|
485
|
+
if (s < beforeOffset && s > bestStart) {
|
|
486
|
+
best = d;
|
|
487
|
+
bestStart = s;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return best;
|
|
492
|
+
}
|
|
493
|
+
function findDestructuringBinding(sourceFile, name, beforeOffset) {
|
|
494
|
+
for (const be of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.BindingElement)) {
|
|
495
|
+
if (be.getName() === name && be.getStart() < beforeOffset)
|
|
496
|
+
return be;
|
|
497
|
+
}
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
function isImported(sourceFile, name) {
|
|
501
|
+
for (const imp of sourceFile.getImportDeclarations()) {
|
|
502
|
+
if (imp.getDefaultImport()?.getText() === name)
|
|
503
|
+
return true;
|
|
504
|
+
if (imp.getNamespaceImport()?.getText() === name)
|
|
505
|
+
return true;
|
|
506
|
+
for (const ni of imp.getNamedImports()) {
|
|
507
|
+
if ((ni.getAliasNode()?.getText() ?? ni.getName()) === name)
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
514
|
+
// CHAIN ANALYSIS HELPERS
|
|
515
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
516
|
+
/** True when the call (or any call in its chain) is a Playwright selector/frame. */
|
|
517
|
+
function isPlaywrightChain(call) {
|
|
518
|
+
let expr = call;
|
|
519
|
+
for (let i = 0; i < 256; i++) {
|
|
520
|
+
expr = unwrap(expr);
|
|
521
|
+
if (ts_morph_1.Node.isCallExpression(expr)) {
|
|
522
|
+
const callee = unwrap(expr.getExpression());
|
|
523
|
+
if (ts_morph_1.Node.isPropertyAccessExpression(callee)) {
|
|
524
|
+
const cat = categorizeMethod(callee.getName());
|
|
525
|
+
if (cat === "selector" || cat === "frame")
|
|
526
|
+
return true;
|
|
527
|
+
expr = callee.getExpression();
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
expr = callee;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Walk a chain to its leftmost receiver. Returns the root-name when the
|
|
539
|
+
* receiver is a page-root, and the receiver Identifier when the chain hangs
|
|
540
|
+
* off another VARIABLE (a cascade link).
|
|
541
|
+
*/
|
|
542
|
+
function analyzeChainRoot(call) {
|
|
543
|
+
const { leftmost } = getLeftmostReceiver(call);
|
|
544
|
+
if (ts_morph_1.Node.isIdentifier(leftmost)) {
|
|
545
|
+
const txt = leftmost.getText();
|
|
546
|
+
if (PAGE_ROOT_NAMES.has(txt))
|
|
547
|
+
return { rootReceiver: txt, receiverVar: null };
|
|
548
|
+
return { rootReceiver: txt, receiverVar: leftmost };
|
|
549
|
+
}
|
|
550
|
+
return { rootReceiver: "page", receiverVar: null };
|
|
551
|
+
}
|
|
552
|
+
/** True when any selector arg in the chain is a template literal w/ ${…}. */
|
|
553
|
+
function containsDynamicSelectorArg(node) {
|
|
554
|
+
for (const call of [
|
|
555
|
+
...(ts_morph_1.Node.isCallExpression(node) ? [node] : []),
|
|
556
|
+
...node.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression),
|
|
557
|
+
]) {
|
|
558
|
+
const arg = call.getArguments()[0];
|
|
559
|
+
if (arg && ts_morph_1.Node.isTemplateExpression(arg))
|
|
560
|
+
return true; // has substitutions
|
|
561
|
+
}
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
/** Strip trailing action/query calls, returning the locator-chain node. */
|
|
565
|
+
function stripTrailingActions(topCall) {
|
|
566
|
+
let expr = unwrap(topCall);
|
|
567
|
+
for (let i = 0; i < 256; i++) {
|
|
568
|
+
if (!ts_morph_1.Node.isCallExpression(expr))
|
|
569
|
+
break;
|
|
570
|
+
const callee = unwrap(expr.getExpression());
|
|
571
|
+
if (ts_morph_1.Node.isPropertyAccessExpression(callee) &&
|
|
572
|
+
categorizeMethod(callee.getName()) === "action") {
|
|
573
|
+
expr = unwrap(callee.getExpression());
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
return expr;
|
|
579
|
+
}
|
|
580
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
581
|
+
// SMALL AST UTILITIES
|
|
582
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
583
|
+
function unwrap(node) {
|
|
584
|
+
let n = node;
|
|
585
|
+
for (let i = 0; i < 64; i++) {
|
|
586
|
+
if (ts_morph_1.Node.isAwaitExpression(n) ||
|
|
587
|
+
ts_morph_1.Node.isParenthesizedExpression(n) ||
|
|
588
|
+
ts_morph_1.Node.isNonNullExpression(n) ||
|
|
589
|
+
ts_morph_1.Node.isAsExpression(n)) {
|
|
590
|
+
n = n.getExpression();
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
return n;
|
|
596
|
+
}
|
|
597
|
+
function isFunctionLike(node) {
|
|
598
|
+
return (ts_morph_1.Node.isArrowFunction(node) ||
|
|
599
|
+
ts_morph_1.Node.isFunctionDeclaration(node) ||
|
|
600
|
+
ts_morph_1.Node.isFunctionExpression(node) ||
|
|
601
|
+
ts_morph_1.Node.isMethodDeclaration(node) ||
|
|
602
|
+
ts_morph_1.Node.isConstructorDeclaration(node) ||
|
|
603
|
+
ts_morph_1.Node.isGetAccessorDeclaration(node) ||
|
|
604
|
+
ts_morph_1.Node.isSetAccessorDeclaration(node));
|
|
605
|
+
}
|
|
606
|
+
function isNullishOrLogical(bin) {
|
|
607
|
+
if (!ts_morph_1.Node.isBinaryExpression(bin))
|
|
608
|
+
return false;
|
|
609
|
+
const op = bin.getOperatorToken().getKind();
|
|
610
|
+
return (op === ts_morph_1.SyntaxKind.QuestionQuestionToken ||
|
|
611
|
+
op === ts_morph_1.SyntaxKind.BarBarToken ||
|
|
612
|
+
op === ts_morph_1.SyntaxKind.AmpersandAmpersandToken);
|
|
613
|
+
}
|
|
614
|
+
function getEnclosingVariableDeclaration(node) {
|
|
615
|
+
let cur = node.getParent();
|
|
616
|
+
while (cur) {
|
|
617
|
+
if (ts_morph_1.Node.isVariableDeclaration(cur))
|
|
618
|
+
return cur;
|
|
619
|
+
if (ts_morph_1.Node.isBlock(cur) || ts_morph_1.Node.isSourceFile(cur) || isFunctionLike(cur)) {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
cur = cur.getParent();
|
|
623
|
+
}
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
function isWithin(node, container) {
|
|
627
|
+
return (node.getStart() >= container.getStart() &&
|
|
628
|
+
node.getEnd() <= container.getEnd());
|
|
629
|
+
}
|
|
630
|
+
/** Detects a standalone reassignment of a let/var-declared name after init. */
|
|
631
|
+
function isReassigned(decl) {
|
|
632
|
+
const kind = decl.getVariableStatement()?.getDeclarationKind();
|
|
633
|
+
// DeclarationKind: "const" is not reassignable.
|
|
634
|
+
if (!kind || kind === "const")
|
|
635
|
+
return false;
|
|
636
|
+
const name = decl.getName();
|
|
637
|
+
const sf = decl.getSourceFile();
|
|
638
|
+
for (const bin of sf.getDescendantsOfKind(ts_morph_1.SyntaxKind.BinaryExpression)) {
|
|
639
|
+
if (bin.getOperatorToken().getKind() !== ts_morph_1.SyntaxKind.EqualsToken)
|
|
640
|
+
continue;
|
|
641
|
+
const left = bin.getLeft();
|
|
642
|
+
if (ts_morph_1.Node.isIdentifier(left) && left.getText() === name)
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
function findConstructorAssignment(cls, propName) {
|
|
648
|
+
for (const bin of cls.getDescendantsOfKind(ts_morph_1.SyntaxKind.BinaryExpression)) {
|
|
649
|
+
if (bin.getOperatorToken().getKind() !== ts_morph_1.SyntaxKind.EqualsToken)
|
|
650
|
+
continue;
|
|
651
|
+
const left = bin.getLeft();
|
|
652
|
+
if (ts_morph_1.Node.isPropertyAccessExpression(left) &&
|
|
653
|
+
left.getExpression().getKind() === ts_morph_1.SyntaxKind.ThisKeyword &&
|
|
654
|
+
left.getName() === propName) {
|
|
655
|
+
return bin.getRight();
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
function makeTarget(declKind, targetNode, declaration, rootReceiver, symbolNameOverride) {
|
|
661
|
+
return {
|
|
662
|
+
declKind,
|
|
663
|
+
targetNode,
|
|
664
|
+
declaration,
|
|
665
|
+
symbolName: symbolNameOverride ?? (0, AnchorResolver_1.enclosingSymbolName)(targetNode),
|
|
666
|
+
rootReceiver,
|
|
667
|
+
anchor: (0, AnchorResolver_1.computeAnchor)(targetNode),
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function abort(severity, code, reason, node) {
|
|
671
|
+
return { status: "abort", severity, code, reason, node };
|
|
672
|
+
}
|