-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathparse-ast.ts
More file actions
200 lines (173 loc) · 7.58 KB
/
Copy pathparse-ast.ts
File metadata and controls
200 lines (173 loc) · 7.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
/**
* AST Parse Learning Script
*
* Section 1: full SyntaxKind tree (printTree)
* Section 2: CallExpression-only walk (printCallExpressions)
*
* Run from repo root:
* npm run parse-ast → reads ./sample.ts
* npm run parse-ast:tsx → reads ./sample.tsx (includes JSX nodes)
*
* Compare the output with https://astexplorer.net (select "typescript").
*/
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { parseSourceFile } from '@sentinel-scan/core'
import * as ts from 'typescript'
// ─── Resolve paths relative to repo root ─────────────────────────────────────
//
// parseSourceFile (from @sentinel-scan/core) reads the file and runs createSourceFile.
// The script only chooses which sample file to parse.
const scriptDir = dirname(fileURLToPath(import.meta.url))
const repoRoot = resolve(scriptDir, '..')
const useTsx = process.argv.includes('--tsx')
const sampleFileName = useTsx ? 'sample.tsx' : 'sample.ts'
const samplePath = join(repoRoot, sampleFileName)
let sourceFile: ts.SourceFile
try {
sourceFile = parseSourceFile(samplePath)
} catch {
console.error(`Could not read or parse ${samplePath}`)
process.exit(1)
}
console.log(`\nParsing: ${sampleFileName}\n${'─'.repeat(60)}\n`)
// ─── Babel "plugins" vs TypeScript's integrated parser ───────────────────────
//
// Babel is a pluggable pipeline: you add @babel/plugin-syntax-jsx,
// @babel/preset-typescript, etc. because Babel's core parser does not know
// about every language extension out of the box.
//
// The TypeScript Compiler API is a single integrated parser. Pass
// ScriptKind.TSX and JSX is parsed natively — no plugin array required.
//
// Note: TS does have "transformers" (ts.transform) for emit/codemods — a
// different concept from Babel plugins. We use neither here; parsing only.
// ─── JSX vs CallExpression (when using sample.tsx) ───────────────────────────
//
// Side-by-side in the AST:
//
// fetch(url) → CallExpression
// └─ expression: Identifier ("fetch")
//
// axios.get(url) → CallExpression
// └─ expression: PropertyAccessExpression (axios.get)
// (Babel calls this MemberExpression)
//
// <span>{name}</span> → JsxElement
// ├─ openingElement: JsxOpeningElement ("span")
// ├─ children: JsxExpression { expression: Identifier }
// └─ closingElement: JsxClosingElement
//
// JSX nodes are NOT CallExpressions in the TS AST. A build step may later
// desugar JSX to React.createElement(...) calls, but the parser preserves
// JsxElement / JsxSelfClosingElement nodes as distinct syntax.
if (useTsx) {
console.log(
'Note: sample.tsx includes JSX — look for JsxElement / JsxOpeningElement nodes.\n' +
' Compare with CallExpression nodes from fetch() and axios.get().\n',
)
}
// ─── Walk the tree and print SyntaxKind names ──────────────────────────────────
//
// ts.forEachChild visits only real AST child nodes (not comment/token noise).
// ts.forEachChild is preferred over node.getChildren() for traversal because
// getChildren() also returns punctuation tokens (parens, semicolons, etc.).
/** Human-readable label for a node kind, e.g. "CallExpression". */
function kindName(node: ts.Node): string {
const name = ts.SyntaxKind[node.kind]
return typeof name === 'string' ? name : `Unknown(${String(node.kind)})`
}
/**
* Optional hint appended to a line for leaf-ish nodes that carry useful text.
* Keeps output readable without dumping raw JSON.
*/
function nodeHint(node: ts.Node): string {
if (ts.isIdentifier(node)) {
return ` (${node.text})`
}
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
return ` ("${node.text}")`
}
if (ts.isNumericLiteral(node)) {
return ` (${node.text})`
}
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
const tag = node.tagName.getText(sourceFile)
return ` (<${tag}>)`
}
if (ts.isJsxClosingElement(node)) {
const tag = node.tagName.getText(sourceFile)
return ` (</${tag}>)`
}
return ''
}
/** Recursively print each node kind, indented by depth. */
function printTree(node: ts.Node, depth = 0): void {
const indent = ' '.repeat(depth)
console.log(`${indent}${kindName(node)}${nodeHint(node)}`)
ts.forEachChild(node, (child) => {
printTree(child, depth + 1)
})
}
printTree(sourceFile)
// ─── CallExpression-only walk ────────────────────────────────────────────────
//
// Same depth-first pre-order as printTree: handle the current node, then walk
// each child left-to-right via ts.forEachChild. printTree does this for every
// SyntaxKind; here we filter with node.kind === ts.SyntaxKind.CallExpression.
//
// Visitor pattern (OOP): frameworks like Babel use objects such as
// { CallExpression(path) { ... } }. The TS Compiler API uses manual dispatch —
// if (ts.isCallExpression(node)) — plus recursive forEachChild. Same idea, no
// visitor class required.
//
// We do NOT use ts.transform / transformer visitors here. Transformers mutate or
// emit AST nodes during compilation. Static analysis only reads the tree; reach
// for ts.transform when codemodding or custom emit.
//
// Traversal order for foo(bar(1), baz()) — verify in sample.ts demoCallOrder:
// 1. foo (depth 0 — outer call)
// 2. bar (depth 1 — first argument)
// 3. baz (depth 1 — second argument)
console.log(`\n${'─'.repeat(60)}`)
console.log('CallExpression walk (pre-order, depth-first)\n')
/** Resolve a human-readable callable name from CallExpression.expression. */
function getCallableName(expression: ts.Expression, file: ts.SourceFile): string {
// Plain call: fetch(url), foo(), get('/api')
if (ts.isIdentifier(expression)) {
return expression.text
}
// Member call: axios.get(url), client.post(url)
if (ts.isPropertyAccessExpression(expression)) {
return expression.getText(file)
}
// obj[method](url), (() => {})(), await fn() — no stable name
return 'anonymous'
}
/** Format 1-indexed line:column for a node (matches Sentinel diagnostics). */
function formatLocation(node: ts.Node, file: ts.SourceFile): string {
const { line, character } = file.getLineAndCharacterOfPosition(node.getStart())
return `${String(line + 1)}:${String(character + 1)}`
}
/**
* Walk the AST and print only CallExpression nodes, including nested calls
* (e.g. bar inside foo(bar(1), baz())).
*/
function printCallExpressions(file: ts.SourceFile, node: ts.Node = file, callDepth = 0): void {
if (node.kind === ts.SyntaxKind.CallExpression) {
const call = node as ts.CallExpression
const indent = ' '.repeat(callDepth)
const name = getCallableName(call.expression, file)
const loc = formatLocation(call, file)
console.log(`${indent}${name} @ ${loc}`)
}
const nextDepth = ts.isCallExpression(node) ? callDepth + 1 : callDepth
ts.forEachChild(node, (child) => {
printCallExpressions(file, child, nextDepth)
})
}
printCallExpressions(sourceFile)
console.log(`\n${'─'.repeat(60)}`)
console.log(
`Done. Root node: ${kindName(sourceFile)} (${String(sourceFile.statements.length)} top-level statements)\n`,
)