@opensip-tools/lang-typescript 1.0.4

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.
Files changed (45) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-typecheck.log +4 -0
  3. package/LICENSE +21 -0
  4. package/dist/__tests__/adapter.test.d.ts +2 -0
  5. package/dist/__tests__/adapter.test.d.ts.map +1 -0
  6. package/dist/__tests__/adapter.test.js +56 -0
  7. package/dist/__tests__/adapter.test.js.map +1 -0
  8. package/dist/__tests__/ast-utilities.test.d.ts +2 -0
  9. package/dist/__tests__/ast-utilities.test.d.ts.map +1 -0
  10. package/dist/__tests__/ast-utilities.test.js +237 -0
  11. package/dist/__tests__/ast-utilities.test.js.map +1 -0
  12. package/dist/adapter.d.ts +6 -0
  13. package/dist/adapter.d.ts.map +1 -0
  14. package/dist/adapter.js +15 -0
  15. package/dist/adapter.js.map +1 -0
  16. package/dist/ast-utilities.d.ts +74 -0
  17. package/dist/ast-utilities.d.ts.map +1 -0
  18. package/dist/ast-utilities.js +217 -0
  19. package/dist/ast-utilities.js.map +1 -0
  20. package/dist/index.d.ts +7 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +9 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/parse.d.ts +10 -0
  25. package/dist/parse.d.ts.map +1 -0
  26. package/dist/parse.js +18 -0
  27. package/dist/parse.js.map +1 -0
  28. package/dist/query.d.ts +4 -0
  29. package/dist/query.d.ts.map +1 -0
  30. package/dist/query.js +74 -0
  31. package/dist/query.js.map +1 -0
  32. package/dist/strip.d.ts +19 -0
  33. package/dist/strip.d.ts.map +1 -0
  34. package/dist/strip.js +26 -0
  35. package/dist/strip.js.map +1 -0
  36. package/package.json +41 -0
  37. package/src/__tests__/adapter.test.ts +67 -0
  38. package/src/__tests__/ast-utilities.test.ts +252 -0
  39. package/src/adapter.ts +21 -0
  40. package/src/ast-utilities.ts +238 -0
  41. package/src/index.ts +26 -0
  42. package/src/parse.ts +22 -0
  43. package/src/query.ts +76 -0
  44. package/src/strip.ts +30 -0
  45. package/tsconfig.json +8 -0
@@ -0,0 +1,252 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ countUnescapedBackticks,
5
+ findBinaryExpressions,
6
+ findCallExpressions,
7
+ findTemplateLiterals,
8
+ getColumn,
9
+ getIdentifierName,
10
+ getLineNumber,
11
+ getPropertyChain,
12
+ getSharedSourceFile,
13
+ isInComment,
14
+ isInStringLiteral,
15
+ isLiteral,
16
+ isPropertyAccess,
17
+ parseSource,
18
+ ts,
19
+ walkNodes,
20
+ } from '../ast-utilities.js';
21
+
22
+ const parse = (content: string) => parseSource(content, 'x.ts');
23
+
24
+ describe('parseSource', () => {
25
+ it('parses valid TypeScript', () => {
26
+ expect(parse('const x = 1;')).not.toBeNull();
27
+ });
28
+
29
+ it('returns null on parse failure', () => {
30
+ // Note: TS parser is permissive — try with a sentinel call that throws.
31
+ // Most invalid syntax still produces a tree; instead exercise the catch
32
+ // path by passing a non-string.
33
+ // Cast to any to bypass the type guard for this test.
34
+ const result = parseSource(undefined as unknown as string, 'x.ts');
35
+ expect(result).toBeNull();
36
+ });
37
+ });
38
+
39
+ describe('getSharedSourceFile', () => {
40
+ it('returns a parsed source file', () => {
41
+ expect(getSharedSourceFile('shared.ts', 'export const x = 1;')).not.toBeNull();
42
+ });
43
+ });
44
+
45
+ describe('walkNodes', () => {
46
+ it('visits every descendant node', () => {
47
+ const sf = parse('const x = 1; const y = 2;');
48
+ if (!sf) throw new Error('parse failed');
49
+ let count = 0;
50
+ walkNodes(sf, () => count++);
51
+ expect(count).toBeGreaterThan(2);
52
+ });
53
+ });
54
+
55
+ describe('getIdentifierName / getPropertyChain', () => {
56
+ it('returns the leaf identifier from an Identifier', () => {
57
+ const sf = parse('foo;');
58
+ if (!sf) throw new Error('parse failed');
59
+ let leaf = '';
60
+ walkNodes(sf, (n) => {
61
+ if (ts.isIdentifier(n) && leaf === '') leaf = getIdentifierName(n);
62
+ });
63
+ expect(leaf).toBe('foo');
64
+ });
65
+
66
+ it('returns the property name from a PropertyAccessExpression', () => {
67
+ const sf = parse('a.b.c;');
68
+ if (!sf) throw new Error('parse failed');
69
+ let result = '';
70
+ walkNodes(sf, (n) => {
71
+ if (ts.isPropertyAccessExpression(n) && result === '') result = getPropertyChain(n);
72
+ });
73
+ expect(result).toBe('a.b.c');
74
+ });
75
+
76
+ it('returns empty string for non-identifier non-property nodes', () => {
77
+ const sf = parse('1 + 2;');
78
+ if (!sf) throw new Error('parse failed');
79
+ let found = '';
80
+ walkNodes(sf, (n) => {
81
+ if (ts.isBinaryExpression(n)) found = getIdentifierName(n);
82
+ });
83
+ expect(found).toBe('');
84
+ });
85
+
86
+ it('getPropertyChain returns empty for non-identifier non-property', () => {
87
+ const sf = parse('1 + 2;');
88
+ if (!sf) throw new Error('parse failed');
89
+ let found = '';
90
+ walkNodes(sf, (n) => {
91
+ if (ts.isBinaryExpression(n) && found === '') found = getPropertyChain(n);
92
+ });
93
+ expect(found).toBe('');
94
+ });
95
+ });
96
+
97
+ describe('getLineNumber / getColumn', () => {
98
+ it('returns 1-based line and 0-based column', () => {
99
+ const sf = parse('\n\nconst x = 1;');
100
+ if (!sf) throw new Error('parse failed');
101
+ let line = 0;
102
+ let col = 0;
103
+ walkNodes(sf, (n) => {
104
+ if (ts.isVariableDeclaration(n)) {
105
+ line = getLineNumber(n, sf);
106
+ col = getColumn(n, sf);
107
+ }
108
+ });
109
+ expect(line).toBe(3);
110
+ expect(col).toBe(6); // "const " (6 chars)
111
+ });
112
+ });
113
+
114
+ describe('isPropertyAccess', () => {
115
+ it('matches when the property name is the right one', () => {
116
+ const sf = parse('foo.bar();');
117
+ if (!sf) throw new Error('parse failed');
118
+ let matched = false;
119
+ walkNodes(sf, (n) => {
120
+ if (ts.isPropertyAccessExpression(n) && isPropertyAccess(n, 'bar')) matched = true;
121
+ });
122
+ expect(matched).toBe(true);
123
+ });
124
+
125
+ it('does not match when the property name differs', () => {
126
+ const sf = parse('foo.bar();');
127
+ if (!sf) throw new Error('parse failed');
128
+ let matched = false;
129
+ walkNodes(sf, (n) => {
130
+ if (ts.isPropertyAccessExpression(n) && isPropertyAccess(n, 'baz')) matched = true;
131
+ });
132
+ expect(matched).toBe(false);
133
+ });
134
+ });
135
+
136
+ describe('isLiteral', () => {
137
+ it.each([
138
+ ['"hi"', true],
139
+ ['42', true],
140
+ ['true', true],
141
+ ['false', true],
142
+ ['null', true],
143
+ ['undefined', true],
144
+ ['x', false],
145
+ ])('isLiteral(%s) === %s', (src, expected) => {
146
+ const sf = parse(`(${src});`);
147
+ if (!sf) throw new Error('parse failed');
148
+ let result: boolean | null = null;
149
+ walkNodes(sf, (n) => {
150
+ if (ts.isParenthesizedExpression(n) && result === null) result = isLiteral(n.expression);
151
+ });
152
+ expect(result).toBe(expected);
153
+ });
154
+ });
155
+
156
+ describe('isInStringLiteral', () => {
157
+ it('returns true for nodes inside a string template', () => {
158
+ const sf = parse('const x = `${foo}`;');
159
+ if (!sf) throw new Error('parse failed');
160
+ let found = false;
161
+ walkNodes(sf, (n) => {
162
+ if (ts.isIdentifier(n) && n.text === 'foo' && isInStringLiteral(n)) found = true;
163
+ });
164
+ expect(found).toBe(true);
165
+ });
166
+
167
+ it('returns false for nodes outside string literals', () => {
168
+ const sf = parse('const x = 1; const y = x;');
169
+ if (!sf) throw new Error('parse failed');
170
+ let foundOutside = false;
171
+ walkNodes(sf, (n) => {
172
+ if (ts.isIdentifier(n) && n.text === 'y' && !isInStringLiteral(n)) foundOutside = true;
173
+ });
174
+ expect(foundOutside).toBe(true);
175
+ });
176
+ });
177
+
178
+ describe('findCallExpressions', () => {
179
+ it('finds matching call sites by object + method name', () => {
180
+ const sf = parse('console.log(1); foo(); console.log(2);');
181
+ if (!sf) throw new Error('parse failed');
182
+ const calls = findCallExpressions(sf, 'console', 'log');
183
+ expect(calls.length).toBe(2);
184
+ });
185
+
186
+ it('matches when objectName is a suffix of the property chain', () => {
187
+ const sf = parse('a.b.console.log(1);');
188
+ if (!sf) throw new Error('parse failed');
189
+ const calls = findCallExpressions(sf, 'console', 'log');
190
+ expect(calls.length).toBe(1);
191
+ });
192
+
193
+ it('returns empty when no matches', () => {
194
+ const sf = parse('foo();');
195
+ if (!sf) throw new Error('parse failed');
196
+ expect(findCallExpressions(sf, 'console', 'log')).toEqual([]);
197
+ });
198
+ });
199
+
200
+ describe('findBinaryExpressions', () => {
201
+ it('finds binary expressions of the given operator kind', () => {
202
+ const sf = parse('a + b; c - d; a + e;');
203
+ if (!sf) throw new Error('parse failed');
204
+ expect(findBinaryExpressions(sf, ts.SyntaxKind.PlusToken).length).toBe(2);
205
+ });
206
+ });
207
+
208
+ describe('findTemplateLiterals', () => {
209
+ it('finds template expressions with interpolations', () => {
210
+ const sf = parse('const x = `${a}${b}`;');
211
+ if (!sf) throw new Error('parse failed');
212
+ expect(findTemplateLiterals(sf).length).toBe(1);
213
+ });
214
+
215
+ it('skips no-substitution templates', () => {
216
+ const sf = parse('const x = `static`;');
217
+ if (!sf) throw new Error('parse failed');
218
+ expect(findTemplateLiterals(sf)).toEqual([]);
219
+ });
220
+ });
221
+
222
+ describe('isInComment', () => {
223
+ it('returns false for a position outside a comment', () => {
224
+ const src = 'const x = 1;\nconst y = 2;';
225
+ const sf = parse(src);
226
+ if (!sf) throw new Error('parse failed');
227
+ const xIdx = src.indexOf('const x');
228
+ expect(isInComment(xIdx, sf)).toBe(false);
229
+ });
230
+
231
+ it('returns true for a position inside a leading block comment', () => {
232
+ const src = '/* block\n comment\n*/\nconst x = 1;';
233
+ const sf = parse(src);
234
+ if (!sf) throw new Error('parse failed');
235
+ const insideIdx = src.indexOf('block');
236
+ expect(isInComment(insideIdx, sf)).toBe(true);
237
+ });
238
+ });
239
+
240
+ describe('countUnescapedBackticks', () => {
241
+ it('counts unescaped backticks', () => {
242
+ expect(countUnescapedBackticks('a `b` c')).toBe(2);
243
+ });
244
+
245
+ it('does not count escaped backticks', () => {
246
+ expect(countUnescapedBackticks('a \\` b')).toBe(0);
247
+ });
248
+
249
+ it('returns 0 when there are none', () => {
250
+ expect(countUnescapedBackticks('plain text')).toBe(0);
251
+ });
252
+ });
package/src/adapter.ts ADDED
@@ -0,0 +1,21 @@
1
+
2
+
3
+ import { parseSource } from './parse.js'
4
+ import { typescriptQuery } from './query.js'
5
+ import { stripComments, stripStrings } from './strip.js'
6
+
7
+ import type { LanguageAdapter } from '@opensip-tools/core/languages/adapter.js'
8
+ import type ts from 'typescript'
9
+
10
+ export const typescriptAdapter: LanguageAdapter<ts.SourceFile, ts.Node> = {
11
+ id: 'typescript',
12
+ fileExtensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
13
+ aliases: ['javascript', 'tsx', 'jsx', 'js'],
14
+ parse: parseSource,
15
+ stripStrings,
16
+ stripComments,
17
+ query: typescriptQuery,
18
+ }
19
+
20
+ /** Plugin contract — exported as the lang plugin's `adapters` array. */
21
+ export const adapters = [typescriptAdapter] as const
@@ -0,0 +1,238 @@
1
+ // @fitness-ignore-file batch-operation-limits -- iterates bounded collections (config entries, registry items, or small analysis results)
2
+ /**
3
+ * @fileoverview Shared AST utilities for fitness checks.
4
+ *
5
+ * Common TypeScript AST operations for source parsing, tree walking,
6
+ * and node inspection. Used by AST-based fitness checks. Lives in
7
+ * @opensip-tools/lang-typescript so the dependency on `typescript` is
8
+ * isolated to the language pack.
9
+ */
10
+
11
+ import { getParseTree } from '@opensip-tools/core/languages/parse-cache.js'
12
+ import * as ts from 'typescript'
13
+
14
+
15
+ import { typescriptAdapter } from './adapter.js'
16
+
17
+ // =============================================================================
18
+ // SOURCE PARSING
19
+ // =============================================================================
20
+
21
+ /**
22
+ * Parse TypeScript/JavaScript source into an AST SourceFile.
23
+ * Returns null on parse failure.
24
+ */
25
+ export function parseSource(content: string, filePath: string): ts.SourceFile | null {
26
+ try {
27
+ return ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true)
28
+ } catch {
29
+ return null
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Cached parse — uses the language-aware parse cache via the registered
35
+ * TS adapter. Falls back to a direct parse when no cache is active.
36
+ */
37
+ export function getSharedSourceFile(filePath: string, content: string): ts.SourceFile | null {
38
+ return getParseTree(typescriptAdapter, filePath, content)
39
+ }
40
+
41
+ // =============================================================================
42
+ // TREE WALKING
43
+ // =============================================================================
44
+
45
+ /**
46
+ * Depth-first walk of all nodes in a SourceFile or subtree.
47
+ */
48
+ export function walkNodes(root: ts.Node, visitor: (node: ts.Node) => void): void {
49
+ function visit(node: ts.Node): void {
50
+ visitor(node)
51
+ ts.forEachChild(node, visit)
52
+ }
53
+ ts.forEachChild(root, visit)
54
+ }
55
+
56
+ // =============================================================================
57
+ // NODE INSPECTION
58
+ // =============================================================================
59
+
60
+ /**
61
+ * Get the leaf identifier text from an expression node.
62
+ */
63
+ export function getIdentifierName(node: ts.Node): string {
64
+ if (ts.isIdentifier(node)) return node.text
65
+ if (ts.isPropertyAccessExpression(node)) return node.name.text
66
+ return ''
67
+ }
68
+
69
+ /**
70
+ * Get the full dotted path of a property access chain.
71
+ */
72
+ export function getPropertyChain(node: ts.Node): string {
73
+ if (ts.isIdentifier(node)) return node.text
74
+ if (ts.isPropertyAccessExpression(node)) {
75
+ return `${getPropertyChain(node.expression)}.${node.name.text}`
76
+ }
77
+ return ''
78
+ }
79
+
80
+ /**
81
+ * Get the 1-indexed line number for a node.
82
+ */
83
+ export function getLineNumber(node: ts.Node, sourceFile: ts.SourceFile): number {
84
+ const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
85
+ return line + 1
86
+ }
87
+
88
+ /**
89
+ * Get the column number (0-indexed) for a node.
90
+ */
91
+ export function getColumn(node: ts.Node, sourceFile: ts.SourceFile): number {
92
+ const { character } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
93
+ return character
94
+ }
95
+
96
+ /**
97
+ * Check if a node is a property access matching a specific property name.
98
+ */
99
+ export function isPropertyAccess(node: ts.Node, propertyName: string): boolean {
100
+ return ts.isPropertyAccessExpression(node) && node.name.text === propertyName
101
+ }
102
+
103
+ /**
104
+ * Check if a node is a literal value.
105
+ */
106
+ export function isLiteral(node: ts.Node): boolean {
107
+ if (ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return true
108
+ if (ts.isNoSubstitutionTemplateLiteral(node)) return true
109
+ if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword)
110
+ return true
111
+ if (node.kind === ts.SyntaxKind.NullKeyword) return true
112
+ if (ts.isIdentifier(node) && node.text === 'undefined') return true
113
+ return false
114
+ }
115
+
116
+ /**
117
+ * Check if a node is inside a string literal or template literal.
118
+ */
119
+ export function isInStringLiteral(node: ts.Node): boolean {
120
+ let current = node.parent
121
+ while (!ts.isSourceFile(current)) {
122
+ if (
123
+ ts.isStringLiteral(current) ||
124
+ ts.isNoSubstitutionTemplateLiteral(current) ||
125
+ ts.isTemplateExpression(current)
126
+ ) {
127
+ return true
128
+ }
129
+ current = current.parent
130
+ }
131
+ return false
132
+ }
133
+
134
+ // =============================================================================
135
+ // NODE FINDERS
136
+ // =============================================================================
137
+
138
+ /**
139
+ * Find all call expressions matching `object.method()` pattern.
140
+ */
141
+ export function findCallExpressions(
142
+ root: ts.Node,
143
+ objectName: string,
144
+ methodName: string,
145
+ ): ts.CallExpression[] {
146
+ const results: ts.CallExpression[] = []
147
+ walkNodes(root, (node) => {
148
+ if (!ts.isCallExpression(node)) return
149
+ const expr = node.expression
150
+ if (!ts.isPropertyAccessExpression(expr)) return
151
+ if (expr.name.text !== methodName) return
152
+ const chain = getPropertyChain(expr.expression)
153
+ if (chain === objectName || chain.endsWith(`.${objectName}`)) {
154
+ results.push(node)
155
+ }
156
+ })
157
+ return results
158
+ }
159
+
160
+ /**
161
+ * Find all binary expressions with a specific operator.
162
+ */
163
+ export function findBinaryExpressions(
164
+ root: ts.Node,
165
+ operator: ts.SyntaxKind,
166
+ ): ts.BinaryExpression[] {
167
+ const results: ts.BinaryExpression[] = []
168
+ walkNodes(root, (node) => {
169
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === operator) {
170
+ results.push(node)
171
+ }
172
+ })
173
+ return results
174
+ }
175
+
176
+ /**
177
+ * Find all template literal expressions (with substitutions).
178
+ */
179
+ export function findTemplateLiterals(root: ts.Node): ts.TemplateExpression[] {
180
+ const results: ts.TemplateExpression[] = []
181
+ walkNodes(root, (node) => {
182
+ if (ts.isTemplateExpression(node)) {
183
+ results.push(node)
184
+ }
185
+ })
186
+ return results
187
+ }
188
+
189
+ // =============================================================================
190
+ // COMMENT DETECTION
191
+ // =============================================================================
192
+
193
+ function isPositionInRanges(position: number, ranges: ts.CommentRange[] | undefined): boolean {
194
+ if (!ranges) return false
195
+ for (const range of ranges) {
196
+ if (position >= range.pos && position < range.end) return true
197
+ }
198
+ return false
199
+ }
200
+
201
+ /**
202
+ * Check if a position in the source falls inside a comment.
203
+ */
204
+ export function isInComment(position: number, sourceFile: ts.SourceFile): boolean {
205
+ const text = sourceFile.getFullText()
206
+ const lineStarts = sourceFile.getLineStarts()
207
+
208
+ for (let i = 0; i < lineStarts.length; i++) {
209
+ const lineStart = lineStarts[i] ?? 0
210
+ const lineEnd = i + 1 < lineStarts.length ? (lineStarts[i + 1] ?? text.length) : text.length
211
+
212
+ if (position < lineStart || position >= lineEnd) continue
213
+
214
+ if (isPositionInRanges(position, ts.getLeadingCommentRanges(text, lineStart))) return true
215
+ if (isPositionInRanges(position, ts.getTrailingCommentRanges(text, lineStart))) return true
216
+ }
217
+
218
+ return false
219
+ }
220
+
221
+ // =============================================================================
222
+ // STRING UTILITIES
223
+ // =============================================================================
224
+
225
+ /**
226
+ * Count unescaped backtick characters in a line.
227
+ */
228
+ export function countUnescapedBackticks(line: string): number {
229
+ let count = 0
230
+ for (let ci = 0; ci < line.length; ci++) {
231
+ if (line[ci] === '`' && (ci === 0 || line[ci - 1] !== '\\')) count++
232
+ }
233
+ return count
234
+ }
235
+
236
+ /** Re-export TypeScript namespace for check authors */
237
+ // eslint-disable-next-line unicorn/prefer-export-from -- `export * as from 'typescript'` is invalid (typescript uses `export =`); the namespace import + named export form is the only working shape
238
+ export { ts }
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ // TypeScript LanguageAdapter for opensip-tools
2
+ export { typescriptAdapter, adapters } from './adapter.js'
3
+ export { parseSource } from './parse.js'
4
+ export { typescriptQuery } from './query.js'
5
+ export { stripStrings, stripComments, filterContent, clearFilterCache } from './strip.js'
6
+ export type { FilteredContent } from './strip.js'
7
+
8
+ // Legacy AST helpers — re-exported so existing TS checks can keep their imports
9
+ // pointing at @opensip-tools/lang-typescript instead of @opensip-tools/core/framework/*
10
+ export {
11
+ getSharedSourceFile,
12
+ walkNodes,
13
+ getIdentifierName,
14
+ getPropertyChain,
15
+ getLineNumber,
16
+ getColumn,
17
+ isPropertyAccess,
18
+ isLiteral,
19
+ isInStringLiteral,
20
+ findCallExpressions,
21
+ findBinaryExpressions,
22
+ findTemplateLiterals,
23
+ isInComment,
24
+ countUnescapedBackticks,
25
+ ts,
26
+ } from './ast-utilities.js'
package/src/parse.ts ADDED
@@ -0,0 +1,22 @@
1
+ import ts from 'typescript'
2
+
3
+ /**
4
+ * Parse TypeScript/JavaScript source into a SourceFile.
5
+ * Returns null on parse failure.
6
+ *
7
+ * Uses ts.ScriptKind.TSX so the same parse path handles .ts and .tsx
8
+ * (and is permissive enough for .js / .jsx).
9
+ */
10
+ export function parseSource(content: string, filePath: string): ts.SourceFile | null {
11
+ try {
12
+ return ts.createSourceFile(
13
+ filePath,
14
+ content,
15
+ ts.ScriptTarget.Latest,
16
+ /* setParentNodes */ true,
17
+ ts.ScriptKind.TSX,
18
+ )
19
+ } catch {
20
+ return null
21
+ }
22
+ }
package/src/query.ts ADDED
@@ -0,0 +1,76 @@
1
+ import ts from 'typescript'
2
+
3
+ import type { LanguageQueryAPI } from '@opensip-tools/core/languages/adapter.js'
4
+ import type { GenericFunction, Import, Location } from '@opensip-tools/core/languages/generic-types.js'
5
+
6
+ function locationOf(sourceFile: ts.SourceFile, node: ts.Node): Location {
7
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
8
+ return { file: sourceFile.fileName, line: line + 1, column: character }
9
+ }
10
+
11
+ function walk(node: ts.Node, visit: (n: ts.Node) => void): void {
12
+ visit(node)
13
+ ts.forEachChild(node, (child) => walk(child, visit))
14
+ }
15
+
16
+ export const typescriptQuery: LanguageQueryAPI<ts.SourceFile, ts.Node> = {
17
+ findFunctions(tree) {
18
+ const out: GenericFunction<ts.Node>[] = []
19
+ walk(tree, (n) => {
20
+ if (
21
+ ts.isFunctionDeclaration(n) ||
22
+ ts.isFunctionExpression(n) ||
23
+ ts.isArrowFunction(n) ||
24
+ ts.isMethodDeclaration(n)
25
+ ) {
26
+ const name = (n as ts.FunctionDeclaration).name?.text ?? null
27
+ out.push({ name, location: locationOf(tree, n), node: n })
28
+ }
29
+ })
30
+ return out
31
+ },
32
+ findImports(tree) {
33
+ const out: Import[] = []
34
+ walk(tree, (n) => {
35
+ if (ts.isImportDeclaration(n) && ts.isStringLiteral(n.moduleSpecifier)) {
36
+ const specifier = n.moduleSpecifier.text
37
+ const names: string[] = []
38
+ const clause = n.importClause
39
+ if (clause?.name) names.push(clause.name.text)
40
+ if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
41
+ for (const elem of clause.namedBindings.elements) names.push(elem.name.text)
42
+ }
43
+ out.push({ specifier, names, location: locationOf(tree, n) })
44
+ }
45
+ })
46
+ return out
47
+ },
48
+ findCallsTo(tree, name) {
49
+ const out: ts.Node[] = []
50
+ walk(tree, (n) => {
51
+ if (ts.isCallExpression(n)) {
52
+ const expr = n.expression
53
+ let target = ''
54
+ if (ts.isIdentifier(expr)) target = expr.text
55
+ else if (ts.isPropertyAccessExpression(expr)) target = expr.name.text
56
+ if (target === name) out.push(n)
57
+ }
58
+ })
59
+ return out
60
+ },
61
+ findStringLiterals(tree) {
62
+ const out: { value: string; location: Location }[] = []
63
+ walk(tree, (n) => {
64
+ if (ts.isStringLiteralLike(n)) {
65
+ out.push({ value: n.text, location: locationOf(tree, n) })
66
+ }
67
+ })
68
+ return out
69
+ },
70
+ getLocation(tree, node) {
71
+ return locationOf(tree, node)
72
+ },
73
+ getText(tree, node) {
74
+ return node.getText(tree)
75
+ },
76
+ }
package/src/strip.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @fileoverview TypeScript string and comment stripping.
3
+ *
4
+ * Implements the LanguageAdapter contract methods stripStrings/stripComments
5
+ * by re-using the rich filterContent implementation in core. Both functions
6
+ * preserve byte length so line/column positions remain stable.
7
+ */
8
+
9
+ import { filterContent } from '@opensip-tools/fitness'
10
+
11
+ /**
12
+ * Replace string literal content with whitespace of equal length.
13
+ * Quote/backtick delimiters are preserved; only the inside is blanked.
14
+ */
15
+ export function stripStrings(content: string): string {
16
+ return filterContent(content).code
17
+ }
18
+
19
+ /**
20
+ * Replace string literals AND comments with whitespace of equal length.
21
+ */
22
+ export function stripComments(content: string): string {
23
+ return filterContent(content).codeNoComments
24
+ }
25
+
26
+ // Re-export filterContent and the FilteredContent type for richer
27
+ // position-aware needs. The clearFilterCache helper is also re-exported
28
+ // for compatibility with any callers that managed it directly.
29
+ export { filterContent, clearFilterCache } from '@opensip-tools/fitness'
30
+ export type { FilteredContent } from '@opensip-tools/fitness'
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }