@tsvm/ts-semantics 0.1.0
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/dist/index.d.ts +19 -0
- package/dist/index.js +344 -0
- package/package.json +34 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ProjectSemanticGraph, TypeFact, ModuleInfo, DependencyEdge, SymbolAlias } from '@tsvm/shared';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
|
|
4
|
+
declare function analyzeProject(tsconfigPath: string, entryPoints?: readonly string[]): ProjectSemanticGraph;
|
|
5
|
+
|
|
6
|
+
declare function extractTypeFacts(sourceFile: ts.SourceFile, checker: ts.TypeChecker): TypeFact[];
|
|
7
|
+
|
|
8
|
+
declare function buildModuleGraph(program: ts.Program, checker: ts.TypeChecker): {
|
|
9
|
+
modules: Map<string, ModuleInfo>;
|
|
10
|
+
dependencyEdges: DependencyEdge[];
|
|
11
|
+
entries: string[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
declare function buildSymbolTable(modules: Map<string, ModuleInfo>): {
|
|
15
|
+
symbolTable: TypeFact[];
|
|
16
|
+
aliases: Map<number, SymbolAlias>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export { analyzeProject, buildModuleGraph, buildSymbolTable, extractTypeFacts };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// src/project.ts
|
|
2
|
+
import ts3 from "typescript";
|
|
3
|
+
import { DiagnosticSeverity } from "@tsvm/shared";
|
|
4
|
+
|
|
5
|
+
// src/module-graph.ts
|
|
6
|
+
import ts2 from "typescript";
|
|
7
|
+
import { TypeFactKind as TypeFactKind2 } from "@tsvm/shared";
|
|
8
|
+
|
|
9
|
+
// src/type-facts.ts
|
|
10
|
+
import ts from "typescript";
|
|
11
|
+
import { TypeFactKind } from "@tsvm/shared";
|
|
12
|
+
function getSourceLocation(node) {
|
|
13
|
+
const sourceFile = node.getSourceFile();
|
|
14
|
+
const start = node.getStart();
|
|
15
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(start);
|
|
16
|
+
return {
|
|
17
|
+
filePath: sourceFile.fileName,
|
|
18
|
+
line,
|
|
19
|
+
column: character,
|
|
20
|
+
offset: start,
|
|
21
|
+
length: node.getEnd() - start
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function extractTypeFacts(sourceFile, checker) {
|
|
25
|
+
const facts = [];
|
|
26
|
+
function visit(node) {
|
|
27
|
+
if (ts.isVariableDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node) || ts.isModuleDeclaration(node) || ts.isParameter(node) || ts.isPropertyDeclaration(node) || ts.isMethodDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node) || ts.isConstructorDeclaration(node)) {
|
|
28
|
+
if (node.name && ts.isIdentifier(node.name)) {
|
|
29
|
+
const symbol = checker.getSymbolAtLocation(node.name);
|
|
30
|
+
if (symbol) {
|
|
31
|
+
const type = checker.getTypeOfSymbolAtLocation(symbol, node);
|
|
32
|
+
const typeText = checker.typeToString(type, node, ts.TypeFormatFlags.NoTruncation);
|
|
33
|
+
let kind = TypeFactKind.Variable;
|
|
34
|
+
if (ts.isFunctionDeclaration(node)) kind = TypeFactKind.Function;
|
|
35
|
+
else if (ts.isClassDeclaration(node)) kind = TypeFactKind.Class;
|
|
36
|
+
else if (ts.isInterfaceDeclaration(node)) kind = TypeFactKind.Interface;
|
|
37
|
+
else if (ts.isTypeAliasDeclaration(node)) kind = TypeFactKind.TypeAlias;
|
|
38
|
+
else if (ts.isEnumDeclaration(node)) kind = TypeFactKind.Enum;
|
|
39
|
+
else if (ts.isModuleDeclaration(node)) kind = TypeFactKind.Namespace;
|
|
40
|
+
else if (ts.isParameter(node)) kind = TypeFactKind.Parameter;
|
|
41
|
+
else if (ts.isPropertyDeclaration(node)) kind = TypeFactKind.Property;
|
|
42
|
+
else if (ts.isMethodDeclaration(node)) kind = TypeFactKind.Method;
|
|
43
|
+
else if (ts.isGetAccessor(node) || ts.isSetAccessor(node)) kind = TypeFactKind.Accessor;
|
|
44
|
+
else if (ts.isConstructorDeclaration(node)) kind = TypeFactKind.Constructor;
|
|
45
|
+
let isExported = false;
|
|
46
|
+
let isAmbient = false;
|
|
47
|
+
if (ts.canHaveModifiers(node)) {
|
|
48
|
+
const modifiers = ts.getModifiers(node);
|
|
49
|
+
isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
50
|
+
isAmbient = modifiers?.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword) ?? false;
|
|
51
|
+
}
|
|
52
|
+
let isGeneric = false;
|
|
53
|
+
const typeParameters = [];
|
|
54
|
+
const constraints = [];
|
|
55
|
+
if ("typeParameters" in node) {
|
|
56
|
+
const nodeWithTP = node;
|
|
57
|
+
const tps = nodeWithTP.typeParameters;
|
|
58
|
+
if (tps) {
|
|
59
|
+
isGeneric = true;
|
|
60
|
+
tps.forEach((tp) => {
|
|
61
|
+
typeParameters.push(tp.name.text);
|
|
62
|
+
if (tp.constraint) {
|
|
63
|
+
constraints.push(tp.constraint.getText());
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const decorators = [];
|
|
69
|
+
if (ts.canHaveDecorators(node)) {
|
|
70
|
+
const decs = ts.getDecorators(node);
|
|
71
|
+
if (decs) {
|
|
72
|
+
decs.forEach((d) => decorators.push(d.expression.getText()));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
facts.push({
|
|
76
|
+
symbolName: symbol.name,
|
|
77
|
+
symbolId: symbol.id ?? -1,
|
|
78
|
+
kind,
|
|
79
|
+
typeText,
|
|
80
|
+
flags: type.flags,
|
|
81
|
+
isGeneric,
|
|
82
|
+
typeParameters,
|
|
83
|
+
constraints,
|
|
84
|
+
sourceLocation: getSourceLocation(node),
|
|
85
|
+
isExported,
|
|
86
|
+
isAmbient,
|
|
87
|
+
decorators
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
ts.forEachChild(node, visit);
|
|
93
|
+
}
|
|
94
|
+
visit(sourceFile);
|
|
95
|
+
return facts;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/module-graph.ts
|
|
99
|
+
function buildModuleGraph(program, checker) {
|
|
100
|
+
const modules = /* @__PURE__ */ new Map();
|
|
101
|
+
const dependencyEdges = [];
|
|
102
|
+
const entries = [];
|
|
103
|
+
const sourceFiles = program.getSourceFiles().filter((sf) => !sf.isDeclarationFile && !sf.fileName.includes("node_modules"));
|
|
104
|
+
for (const sf of sourceFiles) {
|
|
105
|
+
const filePath = sf.fileName;
|
|
106
|
+
const typeFacts = extractTypeFacts(sf, checker);
|
|
107
|
+
const exports = [];
|
|
108
|
+
const imports = [];
|
|
109
|
+
let hasJSX = false;
|
|
110
|
+
let hasDecorators = false;
|
|
111
|
+
ts2.forEachChild(sf, function visit(node) {
|
|
112
|
+
if (ts2.isJsxElement(node) || ts2.isJsxSelfClosingElement(node) || ts2.isJsxFragment(node)) {
|
|
113
|
+
hasJSX = true;
|
|
114
|
+
}
|
|
115
|
+
if (ts2.canHaveDecorators(node) && ts2.getDecorators(node)?.length) {
|
|
116
|
+
hasDecorators = true;
|
|
117
|
+
}
|
|
118
|
+
if (ts2.isImportDeclaration(node)) {
|
|
119
|
+
const moduleSpecifier = node.moduleSpecifier.text;
|
|
120
|
+
const isTypeOnly = node.importClause?.isTypeOnly ?? false;
|
|
121
|
+
if (node.importClause) {
|
|
122
|
+
if (node.importClause.name) {
|
|
123
|
+
imports.push({
|
|
124
|
+
localName: node.importClause.name.text,
|
|
125
|
+
importedName: "default",
|
|
126
|
+
moduleSpecifier,
|
|
127
|
+
kind: "default",
|
|
128
|
+
isTypeOnly
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (node.importClause.namedBindings) {
|
|
132
|
+
if (ts2.isNamedImports(node.importClause.namedBindings)) {
|
|
133
|
+
node.importClause.namedBindings.elements.forEach((el) => {
|
|
134
|
+
imports.push({
|
|
135
|
+
localName: el.name.text,
|
|
136
|
+
importedName: el.propertyName ? el.propertyName.text : el.name.text,
|
|
137
|
+
moduleSpecifier,
|
|
138
|
+
kind: "named",
|
|
139
|
+
isTypeOnly: isTypeOnly || el.isTypeOnly
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
} else if (ts2.isNamespaceImport(node.importClause.namedBindings)) {
|
|
143
|
+
imports.push({
|
|
144
|
+
localName: node.importClause.namedBindings.name.text,
|
|
145
|
+
importedName: "*",
|
|
146
|
+
moduleSpecifier,
|
|
147
|
+
kind: "namespace",
|
|
148
|
+
isTypeOnly
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
imports.push({
|
|
154
|
+
localName: "",
|
|
155
|
+
importedName: "",
|
|
156
|
+
moduleSpecifier,
|
|
157
|
+
kind: "side_effect",
|
|
158
|
+
isTypeOnly: false
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (ts2.isCallExpression(node) && node.expression.kind === ts2.SyntaxKind.ImportKeyword) {
|
|
163
|
+
if (node.arguments.length > 0 && ts2.isStringLiteral(node.arguments[0])) {
|
|
164
|
+
const moduleSpecifier = node.arguments[0].text;
|
|
165
|
+
dependencyEdges.push({
|
|
166
|
+
fromModule: filePath,
|
|
167
|
+
toModule: moduleSpecifier,
|
|
168
|
+
// Will resolve properly later if needed
|
|
169
|
+
symbols: [],
|
|
170
|
+
isTypeOnly: false,
|
|
171
|
+
isDynamic: true
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (ts2.isExportDeclaration(node)) {
|
|
176
|
+
const isTypeOnly = node.isTypeOnly;
|
|
177
|
+
if (node.exportClause && ts2.isNamedExports(node.exportClause)) {
|
|
178
|
+
node.exportClause.elements.forEach((el) => {
|
|
179
|
+
exports.push({
|
|
180
|
+
localName: el.propertyName ? el.propertyName.text : el.name.text,
|
|
181
|
+
exportedName: el.name.text,
|
|
182
|
+
kind: TypeFactKind2.Variable,
|
|
183
|
+
// Approximate, could be refined
|
|
184
|
+
isTypeOnly: isTypeOnly || el.isTypeOnly,
|
|
185
|
+
isDefault: el.name.text === "default",
|
|
186
|
+
isReExport: !!node.moduleSpecifier,
|
|
187
|
+
sourceModule: node.moduleSpecifier ? node.moduleSpecifier.text : void 0
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
} else if (ts2.canHaveModifiers(node) && ts2.getModifiers(node)?.some((m) => m.kind === ts2.SyntaxKind.ExportKeyword)) {
|
|
192
|
+
const isDefault = ts2.getModifiers(node).some((m) => m.kind === ts2.SyntaxKind.DefaultKeyword);
|
|
193
|
+
let name = "default";
|
|
194
|
+
if (!isDefault && node.name && ts2.isIdentifier(node.name)) {
|
|
195
|
+
name = node.name.text;
|
|
196
|
+
} else if (!isDefault && ts2.isVariableStatement(node)) {
|
|
197
|
+
node.declarationList.declarations.forEach((d) => {
|
|
198
|
+
if (ts2.isIdentifier(d.name)) {
|
|
199
|
+
exports.push({
|
|
200
|
+
localName: d.name.text,
|
|
201
|
+
exportedName: d.name.text,
|
|
202
|
+
kind: TypeFactKind2.Variable,
|
|
203
|
+
isTypeOnly: false,
|
|
204
|
+
isDefault: false,
|
|
205
|
+
isReExport: false
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (node.name || isDefault) {
|
|
212
|
+
exports.push({
|
|
213
|
+
localName: isDefault && node.name ? node.name.text : name,
|
|
214
|
+
exportedName: name,
|
|
215
|
+
kind: TypeFactKind2.Function,
|
|
216
|
+
// Approximate
|
|
217
|
+
isTypeOnly: ts2.isInterfaceDeclaration(node) || ts2.isTypeAliasDeclaration(node),
|
|
218
|
+
isDefault,
|
|
219
|
+
isReExport: false
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
ts2.forEachChild(node, visit);
|
|
224
|
+
});
|
|
225
|
+
const isEntryPoint = exports.length > 0;
|
|
226
|
+
if (isEntryPoint) entries.push(filePath);
|
|
227
|
+
modules.set(filePath, {
|
|
228
|
+
filePath,
|
|
229
|
+
relativePath: program.getCurrentDirectory() ? filePath.replace(program.getCurrentDirectory(), "") : filePath,
|
|
230
|
+
exports,
|
|
231
|
+
imports,
|
|
232
|
+
typeFacts,
|
|
233
|
+
isEntryPoint,
|
|
234
|
+
isDeclarationFile: sf.isDeclarationFile,
|
|
235
|
+
hasJSX,
|
|
236
|
+
hasDecorators,
|
|
237
|
+
byteSize: sf.text.length
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
for (const [filePath, mod] of modules.entries()) {
|
|
241
|
+
const targetMap = /* @__PURE__ */ new Map();
|
|
242
|
+
for (const imp of mod.imports) {
|
|
243
|
+
const target = imp.moduleSpecifier;
|
|
244
|
+
if (!targetMap.has(target)) {
|
|
245
|
+
targetMap.set(target, { symbols: [], isTypeOnly: true });
|
|
246
|
+
}
|
|
247
|
+
const entry = targetMap.get(target);
|
|
248
|
+
entry.symbols.push(imp.importedName);
|
|
249
|
+
if (!imp.isTypeOnly) entry.isTypeOnly = false;
|
|
250
|
+
}
|
|
251
|
+
for (const [target, info] of targetMap.entries()) {
|
|
252
|
+
dependencyEdges.push({
|
|
253
|
+
fromModule: filePath,
|
|
254
|
+
toModule: target,
|
|
255
|
+
symbols: info.symbols,
|
|
256
|
+
isTypeOnly: info.isTypeOnly,
|
|
257
|
+
isDynamic: false
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return { modules, dependencyEdges, entries };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/symbol-table.ts
|
|
265
|
+
import { ScopeKind } from "@tsvm/shared";
|
|
266
|
+
function buildSymbolTable(modules) {
|
|
267
|
+
const symbolTable = [];
|
|
268
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
269
|
+
let nextSymbolId = 1;
|
|
270
|
+
for (const [filePath, mod] of modules.entries()) {
|
|
271
|
+
for (const fact of mod.typeFacts) {
|
|
272
|
+
const normalizedId = nextSymbolId++;
|
|
273
|
+
const normalizedFact = {
|
|
274
|
+
...fact,
|
|
275
|
+
symbolId: normalizedId
|
|
276
|
+
};
|
|
277
|
+
symbolTable.push(normalizedFact);
|
|
278
|
+
aliases.set(normalizedId, {
|
|
279
|
+
originalName: fact.symbolName,
|
|
280
|
+
obfuscatedName: fact.symbolName,
|
|
281
|
+
// Will be changed by SymbolIndirectionPass
|
|
282
|
+
scope: fact.isExported ? ScopeKind.Module : ScopeKind.Block,
|
|
283
|
+
// Simplified
|
|
284
|
+
symbolId: normalizedId,
|
|
285
|
+
isExported: fact.isExported
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return { symbolTable, aliases };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/project.ts
|
|
293
|
+
import path from "path";
|
|
294
|
+
function analyzeProject(tsconfigPath, entryPoints) {
|
|
295
|
+
const diagnostics = [];
|
|
296
|
+
const configFile = ts3.readConfigFile(tsconfigPath, ts3.sys.readFile);
|
|
297
|
+
if (configFile.error) {
|
|
298
|
+
throw new Error(`Failed to read tsconfig: ${configFile.error.messageText}`);
|
|
299
|
+
}
|
|
300
|
+
const parsedConfig = ts3.parseJsonConfigFileContent(configFile.config, ts3.sys, path.dirname(tsconfigPath));
|
|
301
|
+
if (parsedConfig.errors.length > 0) {
|
|
302
|
+
const errorMsg = parsedConfig.errors.map((e) => e.messageText).join(", ");
|
|
303
|
+
throw new Error(`Invalid tsconfig: ${errorMsg}`);
|
|
304
|
+
}
|
|
305
|
+
const rootNames = entryPoints && entryPoints.length > 0 ? entryPoints : parsedConfig.fileNames;
|
|
306
|
+
const program = ts3.createProgram({
|
|
307
|
+
rootNames,
|
|
308
|
+
options: parsedConfig.options
|
|
309
|
+
});
|
|
310
|
+
const checker = program.getTypeChecker();
|
|
311
|
+
const { modules, dependencyEdges, entries } = buildModuleGraph(program, checker);
|
|
312
|
+
const { symbolTable, aliases } = buildSymbolTable(modules);
|
|
313
|
+
const tsDiagnostics = ts3.getPreEmitDiagnostics(program);
|
|
314
|
+
for (const diag of tsDiagnostics) {
|
|
315
|
+
diagnostics.push({
|
|
316
|
+
severity: diag.category === 1 ? DiagnosticSeverity.Error : diag.category === 0 ? DiagnosticSeverity.Warning : DiagnosticSeverity.Info,
|
|
317
|
+
code: `TS${diag.code}`,
|
|
318
|
+
message: ts3.flattenDiagnosticMessageText(diag.messageText, "\n"),
|
|
319
|
+
location: diag.file ? {
|
|
320
|
+
filePath: diag.file.fileName,
|
|
321
|
+
line: diag.file.getLineAndCharacterOfPosition(diag.start).line,
|
|
322
|
+
column: diag.file.getLineAndCharacterOfPosition(diag.start).character,
|
|
323
|
+
offset: diag.start,
|
|
324
|
+
length: diag.length
|
|
325
|
+
} : void 0
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
rootDir: program.getCurrentDirectory(),
|
|
330
|
+
modules,
|
|
331
|
+
dependencyEdges,
|
|
332
|
+
entryPoints: entries,
|
|
333
|
+
symbolTable,
|
|
334
|
+
aliases,
|
|
335
|
+
compilerOptions: parsedConfig.options,
|
|
336
|
+
diagnostics
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
export {
|
|
340
|
+
analyzeProject,
|
|
341
|
+
buildModuleGraph,
|
|
342
|
+
buildSymbolTable,
|
|
343
|
+
extractTypeFacts
|
|
344
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tsvm/ts-semantics",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"files": [
|
|
5
|
+
"dist",
|
|
6
|
+
"LICENSE"
|
|
7
|
+
],
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup ./src/index.ts --format esm --dts --clean",
|
|
23
|
+
"test": "pnpm --dir ../.. exec vitest run packages/ts-semantics/tests/**/*.test.ts --config vitest.config.mjs",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"clean": "rm -rf dist"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@tsvm/shared": "workspace:*",
|
|
29
|
+
"typescript": "^5.8.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"tsup": "^8.4.0"
|
|
33
|
+
}
|
|
34
|
+
}
|