@tsonic/frontend 0.0.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/package.json +53 -0
- package/src/dependency-graph.ts +18 -0
- package/src/dotnet-metadata.ts +121 -0
- package/src/graph/builder.ts +81 -0
- package/src/graph/circular.ts +58 -0
- package/src/graph/extraction/exports.ts +55 -0
- package/src/graph/extraction/imports.ts +81 -0
- package/src/graph/extraction/index.ts +7 -0
- package/src/graph/extraction/orchestrator.ts +99 -0
- package/src/graph/extraction.ts +10 -0
- package/src/graph/helpers.ts +51 -0
- package/src/graph/index.ts +17 -0
- package/src/graph/types.ts +13 -0
- package/src/index.ts +80 -0
- package/src/ir/binding-resolution.test.ts +585 -0
- package/src/ir/builder/exports.ts +78 -0
- package/src/ir/builder/helpers.ts +27 -0
- package/src/ir/builder/imports.ts +153 -0
- package/src/ir/builder/index.ts +10 -0
- package/src/ir/builder/orchestrator.ts +178 -0
- package/src/ir/builder/statements.ts +55 -0
- package/src/ir/builder/types.ts +8 -0
- package/src/ir/builder/validation.ts +129 -0
- package/src/ir/builder.test.ts +581 -0
- package/src/ir/builder.ts +14 -0
- package/src/ir/converters/expressions/access.ts +99 -0
- package/src/ir/converters/expressions/calls.ts +137 -0
- package/src/ir/converters/expressions/collections.ts +84 -0
- package/src/ir/converters/expressions/functions.ts +62 -0
- package/src/ir/converters/expressions/helpers.ts +264 -0
- package/src/ir/converters/expressions/index.ts +43 -0
- package/src/ir/converters/expressions/literals.ts +22 -0
- package/src/ir/converters/expressions/operators.ts +147 -0
- package/src/ir/converters/expressions/other.ts +60 -0
- package/src/ir/converters/statements/control/blocks.ts +22 -0
- package/src/ir/converters/statements/control/conditionals.ts +67 -0
- package/src/ir/converters/statements/control/exceptions.ts +43 -0
- package/src/ir/converters/statements/control/index.ts +17 -0
- package/src/ir/converters/statements/control/loops.ts +99 -0
- package/src/ir/converters/statements/control.ts +17 -0
- package/src/ir/converters/statements/declarations/classes/constructors.ts +120 -0
- package/src/ir/converters/statements/declarations/classes/index.ts +12 -0
- package/src/ir/converters/statements/declarations/classes/methods.ts +61 -0
- package/src/ir/converters/statements/declarations/classes/orchestrator.ts +166 -0
- package/src/ir/converters/statements/declarations/classes/override-detection.ts +116 -0
- package/src/ir/converters/statements/declarations/classes/properties.ts +63 -0
- package/src/ir/converters/statements/declarations/classes.ts +6 -0
- package/src/ir/converters/statements/declarations/enums.ts +29 -0
- package/src/ir/converters/statements/declarations/functions.ts +39 -0
- package/src/ir/converters/statements/declarations/index.ts +14 -0
- package/src/ir/converters/statements/declarations/interfaces.ts +131 -0
- package/src/ir/converters/statements/declarations/registry.ts +45 -0
- package/src/ir/converters/statements/declarations/type-aliases.ts +25 -0
- package/src/ir/converters/statements/declarations/variables.ts +60 -0
- package/src/ir/converters/statements/declarations.ts +16 -0
- package/src/ir/converters/statements/helpers.ts +174 -0
- package/src/ir/converters/statements/index.ts +40 -0
- package/src/ir/expression-converter.ts +207 -0
- package/src/ir/generic-validator.ts +100 -0
- package/src/ir/hierarchical-bindings-e2e.test.ts +163 -0
- package/src/ir/index.ts +6 -0
- package/src/ir/statement-converter.ts +128 -0
- package/src/ir/type-converter/arrays.ts +20 -0
- package/src/ir/type-converter/converter.ts +10 -0
- package/src/ir/type-converter/functions.ts +22 -0
- package/src/ir/type-converter/index.ts +11 -0
- package/src/ir/type-converter/inference.ts +122 -0
- package/src/ir/type-converter/literals.ts +40 -0
- package/src/ir/type-converter/objects.ts +107 -0
- package/src/ir/type-converter/orchestrator.ts +85 -0
- package/src/ir/type-converter/patterns.ts +73 -0
- package/src/ir/type-converter/primitives.ts +57 -0
- package/src/ir/type-converter/references.ts +64 -0
- package/src/ir/type-converter/unions-intersections.ts +34 -0
- package/src/ir/type-converter.ts +13 -0
- package/src/ir/types/expressions.ts +215 -0
- package/src/ir/types/guards.ts +39 -0
- package/src/ir/types/helpers.ts +135 -0
- package/src/ir/types/index.ts +108 -0
- package/src/ir/types/ir-types.ts +96 -0
- package/src/ir/types/module.ts +57 -0
- package/src/ir/types/statements.ts +238 -0
- package/src/ir/types.ts +97 -0
- package/src/metadata/bindings-loader.test.ts +144 -0
- package/src/metadata/bindings-loader.ts +357 -0
- package/src/metadata/index.ts +15 -0
- package/src/metadata/library-loader.ts +153 -0
- package/src/metadata/loader.test.ts +156 -0
- package/src/metadata/loader.ts +382 -0
- package/src/program/bindings.test.ts +512 -0
- package/src/program/bindings.ts +253 -0
- package/src/program/config.ts +30 -0
- package/src/program/creation.ts +249 -0
- package/src/program/dependency-graph.ts +245 -0
- package/src/program/diagnostics.ts +103 -0
- package/src/program/index.ts +19 -0
- package/src/program/metadata.ts +68 -0
- package/src/program/queries.ts +18 -0
- package/src/program/types.ts +38 -0
- package/src/program.ts +13 -0
- package/src/resolver/dotnet-import-resolver.ts +226 -0
- package/src/resolver/import-resolution.ts +177 -0
- package/src/resolver/index.ts +18 -0
- package/src/resolver/namespace.test.ts +86 -0
- package/src/resolver/namespace.ts +42 -0
- package/src/resolver/naming.ts +38 -0
- package/src/resolver/path-resolution.ts +22 -0
- package/src/resolver/types.ts +15 -0
- package/src/resolver.test.ts +155 -0
- package/src/resolver.ts +14 -0
- package/src/symbol-table/builder.ts +114 -0
- package/src/symbol-table/creation.ts +42 -0
- package/src/symbol-table/helpers.ts +18 -0
- package/src/symbol-table/index.ts +13 -0
- package/src/symbol-table/queries.ts +42 -0
- package/src/symbol-table/types.ts +28 -0
- package/src/symbol-table.ts +14 -0
- package/src/types/bindings.ts +172 -0
- package/src/types/diagnostic.test.ts +164 -0
- package/src/types/diagnostic.ts +153 -0
- package/src/types/explicit-views.test.ts +113 -0
- package/src/types/explicit-views.ts +218 -0
- package/src/types/metadata.ts +229 -0
- package/src/types/module.ts +99 -0
- package/src/types/nested-types.test.ts +194 -0
- package/src/types/nested-types.ts +215 -0
- package/src/types/parameter-modifiers.ts +173 -0
- package/src/types/ref-parameters.test.ts +192 -0
- package/src/types/ref-parameters.ts +268 -0
- package/src/types/result.test.ts +157 -0
- package/src/types/result.ts +48 -0
- package/src/types/support-types.test.ts +81 -0
- package/src/types/support-types.ts +288 -0
- package/src/types/test-harness.ts +180 -0
- package/src/validation/exports.ts +98 -0
- package/src/validation/features.ts +89 -0
- package/src/validation/generics.ts +40 -0
- package/src/validation/helpers.ts +31 -0
- package/src/validation/imports.ts +97 -0
- package/src/validation/index.ts +11 -0
- package/src/validation/orchestrator.ts +51 -0
- package/src/validation/static-safety.ts +267 -0
- package/src/validator.test.ts +468 -0
- package/src/validator.ts +15 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library Loader - Load metadata and bindings from external library directories.
|
|
3
|
+
*
|
|
4
|
+
* External libraries (specified via --lib flag) contain both metadata and bindings:
|
|
5
|
+
* lib-path/
|
|
6
|
+
* .metadata/
|
|
7
|
+
* Namespace.metadata.json
|
|
8
|
+
* .bindings/
|
|
9
|
+
* Namespace.bindings.json
|
|
10
|
+
*
|
|
11
|
+
* This module provides helpers to load both from a single library root path.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { existsSync, statSync } from "fs";
|
|
16
|
+
import type { Result } from "../types/result.js";
|
|
17
|
+
import type { Diagnostic } from "../types/diagnostic.js";
|
|
18
|
+
import { loadMetadataDirectory } from "./loader.js";
|
|
19
|
+
import { loadBindingsDirectory } from "./bindings-loader.js";
|
|
20
|
+
import type { MetadataFile } from "../types/metadata.js";
|
|
21
|
+
import type { BindingsFile } from "../types/bindings.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Loaded library data (metadata + bindings).
|
|
25
|
+
*/
|
|
26
|
+
export type LibraryData = {
|
|
27
|
+
readonly metadata: ReadonlyArray<MetadataFile>;
|
|
28
|
+
readonly bindings: ReadonlyArray<BindingsFile>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load metadata and bindings from a library directory.
|
|
33
|
+
*
|
|
34
|
+
* Looks for:
|
|
35
|
+
* - ${libraryPath}/.metadata/ directory
|
|
36
|
+
* - ${libraryPath}/.bindings/ directory
|
|
37
|
+
*
|
|
38
|
+
* @param libraryPath - Path to external library root
|
|
39
|
+
* @returns Result with library data or diagnostics
|
|
40
|
+
*/
|
|
41
|
+
export const loadLibrary = (
|
|
42
|
+
libraryPath: string
|
|
43
|
+
): Result<LibraryData, Diagnostic[]> => {
|
|
44
|
+
const diagnostics: Diagnostic[] = [];
|
|
45
|
+
|
|
46
|
+
// Validate library path exists
|
|
47
|
+
if (!existsSync(libraryPath)) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
error: [
|
|
51
|
+
{
|
|
52
|
+
code: "TSN9016",
|
|
53
|
+
severity: "error",
|
|
54
|
+
message: `Library directory not found: ${libraryPath}`,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Validate it's a directory
|
|
61
|
+
if (!statSync(libraryPath).isDirectory()) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
error: [
|
|
65
|
+
{
|
|
66
|
+
code: "TSN9017",
|
|
67
|
+
severity: "error",
|
|
68
|
+
message: `Library path is not a directory: ${libraryPath}`,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Load metadata from .metadata/ subdirectory
|
|
75
|
+
const metadataPath = join(libraryPath, ".metadata");
|
|
76
|
+
let metadata: ReadonlyArray<MetadataFile> = [];
|
|
77
|
+
|
|
78
|
+
if (existsSync(metadataPath)) {
|
|
79
|
+
const metadataResult = loadMetadataDirectory(metadataPath);
|
|
80
|
+
if (metadataResult.ok) {
|
|
81
|
+
metadata = metadataResult.value;
|
|
82
|
+
} else {
|
|
83
|
+
diagnostics.push(...metadataResult.error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Load bindings from .bindings/ subdirectory
|
|
88
|
+
const bindingsPath = join(libraryPath, ".bindings");
|
|
89
|
+
let bindings: ReadonlyArray<BindingsFile> = [];
|
|
90
|
+
|
|
91
|
+
if (existsSync(bindingsPath)) {
|
|
92
|
+
const bindingsResult = loadBindingsDirectory(bindingsPath);
|
|
93
|
+
if (bindingsResult.ok) {
|
|
94
|
+
bindings = bindingsResult.value;
|
|
95
|
+
} else {
|
|
96
|
+
diagnostics.push(...bindingsResult.error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Return success even if one or both are missing (library might be incomplete)
|
|
101
|
+
return {
|
|
102
|
+
ok: true,
|
|
103
|
+
value: {
|
|
104
|
+
metadata,
|
|
105
|
+
bindings,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Load metadata and bindings from multiple library directories.
|
|
112
|
+
*
|
|
113
|
+
* @param libraryPaths - Array of library root paths
|
|
114
|
+
* @returns Result with merged library data or diagnostics
|
|
115
|
+
*/
|
|
116
|
+
export const loadLibraries = (
|
|
117
|
+
libraryPaths: readonly string[]
|
|
118
|
+
): Result<LibraryData, Diagnostic[]> => {
|
|
119
|
+
const allMetadata: MetadataFile[] = [];
|
|
120
|
+
const allBindings: BindingsFile[] = [];
|
|
121
|
+
const allDiagnostics: Diagnostic[] = [];
|
|
122
|
+
|
|
123
|
+
for (const libPath of libraryPaths) {
|
|
124
|
+
const result = loadLibrary(libPath);
|
|
125
|
+
if (result.ok) {
|
|
126
|
+
allMetadata.push(...result.value.metadata);
|
|
127
|
+
allBindings.push(...result.value.bindings);
|
|
128
|
+
} else {
|
|
129
|
+
allDiagnostics.push(...result.error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// If we have any diagnostics but also loaded some data, still succeed
|
|
134
|
+
// (allows partial library loading)
|
|
135
|
+
if (
|
|
136
|
+
allDiagnostics.length > 0 &&
|
|
137
|
+
allMetadata.length === 0 &&
|
|
138
|
+
allBindings.length === 0
|
|
139
|
+
) {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
error: allDiagnostics,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
ok: true,
|
|
148
|
+
value: {
|
|
149
|
+
metadata: allMetadata,
|
|
150
|
+
bindings: allBindings,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for metadata JSON loader
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it } from "mocha";
|
|
6
|
+
import { strict as assert } from "assert";
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import * as os from "os";
|
|
10
|
+
import { loadMetadataFile, loadMetadataDirectory } from "./loader.js";
|
|
11
|
+
import type { MetadataFile } from "../types/metadata.js";
|
|
12
|
+
|
|
13
|
+
describe("Metadata Loader", () => {
|
|
14
|
+
describe("loadMetadataFile", () => {
|
|
15
|
+
it("should load valid metadata file", () => {
|
|
16
|
+
// Create temporary metadata file
|
|
17
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tsonic-test-"));
|
|
18
|
+
const metadataPath = path.join(tmpDir, "test.metadata.json");
|
|
19
|
+
|
|
20
|
+
const metadata: MetadataFile = {
|
|
21
|
+
namespace: "System",
|
|
22
|
+
contributingAssemblies: ["System.Runtime"],
|
|
23
|
+
types: [
|
|
24
|
+
{
|
|
25
|
+
clrName: "System.String",
|
|
26
|
+
tsEmitName: "String",
|
|
27
|
+
kind: "Class",
|
|
28
|
+
accessibility: "Public",
|
|
29
|
+
isAbstract: false,
|
|
30
|
+
isSealed: true,
|
|
31
|
+
isStatic: false,
|
|
32
|
+
arity: 0,
|
|
33
|
+
methods: [],
|
|
34
|
+
properties: [],
|
|
35
|
+
fields: [],
|
|
36
|
+
events: [],
|
|
37
|
+
constructors: [],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
|
|
43
|
+
|
|
44
|
+
const result = loadMetadataFile(metadataPath);
|
|
45
|
+
|
|
46
|
+
// Cleanup
|
|
47
|
+
fs.unlinkSync(metadataPath);
|
|
48
|
+
fs.rmdirSync(tmpDir);
|
|
49
|
+
|
|
50
|
+
assert.ok(result.ok);
|
|
51
|
+
if (result.ok) {
|
|
52
|
+
assert.equal(result.value.namespace, "System");
|
|
53
|
+
assert.equal(result.value.types.length, 1);
|
|
54
|
+
assert.equal(result.value.types[0]?.tsEmitName, "String");
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should return error for non-existent file", () => {
|
|
59
|
+
const result = loadMetadataFile("/nonexistent/file.metadata.json");
|
|
60
|
+
|
|
61
|
+
assert.ok(!result.ok);
|
|
62
|
+
if (!result.ok) {
|
|
63
|
+
assert.equal(result.error[0]?.code, "TSN9001");
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should return error for invalid JSON", () => {
|
|
68
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tsonic-test-"));
|
|
69
|
+
const metadataPath = path.join(tmpDir, "invalid.metadata.json");
|
|
70
|
+
|
|
71
|
+
fs.writeFileSync(metadataPath, "{ invalid json }");
|
|
72
|
+
|
|
73
|
+
const result = loadMetadataFile(metadataPath);
|
|
74
|
+
|
|
75
|
+
// Cleanup
|
|
76
|
+
fs.unlinkSync(metadataPath);
|
|
77
|
+
fs.rmdirSync(tmpDir);
|
|
78
|
+
|
|
79
|
+
assert.ok(!result.ok);
|
|
80
|
+
if (!result.ok) {
|
|
81
|
+
assert.equal(result.error[0]?.code, "TSN9003");
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should return error for missing required fields", () => {
|
|
86
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tsonic-test-"));
|
|
87
|
+
const metadataPath = path.join(tmpDir, "incomplete.metadata.json");
|
|
88
|
+
|
|
89
|
+
// Missing namespace field
|
|
90
|
+
fs.writeFileSync(
|
|
91
|
+
metadataPath,
|
|
92
|
+
JSON.stringify({ contributingAssemblies: [], types: [] })
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const result = loadMetadataFile(metadataPath);
|
|
96
|
+
|
|
97
|
+
// Cleanup
|
|
98
|
+
fs.unlinkSync(metadataPath);
|
|
99
|
+
fs.rmdirSync(tmpDir);
|
|
100
|
+
|
|
101
|
+
assert.ok(!result.ok);
|
|
102
|
+
if (!result.ok) {
|
|
103
|
+
assert.equal(result.error[0]?.code, "TSN9005");
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("loadMetadataDirectory", () => {
|
|
109
|
+
it("should load all metadata files from directory", () => {
|
|
110
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tsonic-test-"));
|
|
111
|
+
|
|
112
|
+
// Create two metadata files
|
|
113
|
+
const metadata1: MetadataFile = {
|
|
114
|
+
namespace: "System",
|
|
115
|
+
contributingAssemblies: ["System.Runtime"],
|
|
116
|
+
types: [],
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const metadata2: MetadataFile = {
|
|
120
|
+
namespace: "System.Collections",
|
|
121
|
+
contributingAssemblies: ["System.Collections"],
|
|
122
|
+
types: [],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
fs.writeFileSync(
|
|
126
|
+
path.join(tmpDir, "System.metadata.json"),
|
|
127
|
+
JSON.stringify(metadata1)
|
|
128
|
+
);
|
|
129
|
+
fs.writeFileSync(
|
|
130
|
+
path.join(tmpDir, "System.Collections.metadata.json"),
|
|
131
|
+
JSON.stringify(metadata2)
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const result = loadMetadataDirectory(tmpDir);
|
|
135
|
+
|
|
136
|
+
// Cleanup
|
|
137
|
+
fs.unlinkSync(path.join(tmpDir, "System.metadata.json"));
|
|
138
|
+
fs.unlinkSync(path.join(tmpDir, "System.Collections.metadata.json"));
|
|
139
|
+
fs.rmdirSync(tmpDir);
|
|
140
|
+
|
|
141
|
+
assert.ok(result.ok);
|
|
142
|
+
if (result.ok) {
|
|
143
|
+
assert.equal(result.value.length, 2);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should return error for non-existent directory", () => {
|
|
148
|
+
const result = loadMetadataDirectory("/nonexistent/directory");
|
|
149
|
+
|
|
150
|
+
assert.ok(!result.ok);
|
|
151
|
+
if (!result.ok) {
|
|
152
|
+
assert.equal(result.error[0]?.code, "TSN9016");
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata JSON loader - Reads and validates .metadata.json files from tsbindgen.
|
|
3
|
+
*
|
|
4
|
+
* This module provides pure functions to load CLR metadata files and validate
|
|
5
|
+
* their structure against the expected schema.
|
|
6
|
+
*
|
|
7
|
+
* @see spec/metadata.md for complete schema documentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import type { Result } from "../types/result.ts";
|
|
13
|
+
import type { Diagnostic } from "../types/diagnostic.ts";
|
|
14
|
+
import type {
|
|
15
|
+
MetadataFile,
|
|
16
|
+
TypeMetadata,
|
|
17
|
+
TypeKind,
|
|
18
|
+
Accessibility,
|
|
19
|
+
} from "../types/metadata.ts";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Load and parse a metadata.json file.
|
|
23
|
+
*
|
|
24
|
+
* @param filePath - Absolute path to the .metadata.json file
|
|
25
|
+
* @returns Result containing parsed metadata or diagnostics
|
|
26
|
+
*/
|
|
27
|
+
export const loadMetadataFile = (
|
|
28
|
+
filePath: string
|
|
29
|
+
): Result<MetadataFile, Diagnostic[]> => {
|
|
30
|
+
// Check file exists
|
|
31
|
+
if (!fs.existsSync(filePath)) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
error: [
|
|
35
|
+
{
|
|
36
|
+
code: "TSN9001",
|
|
37
|
+
message: `Metadata file not found: ${filePath}`,
|
|
38
|
+
severity: "error",
|
|
39
|
+
location: undefined,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Read file contents
|
|
46
|
+
let content: string;
|
|
47
|
+
try {
|
|
48
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
error: [
|
|
53
|
+
{
|
|
54
|
+
code: "TSN9002",
|
|
55
|
+
message: `Failed to read metadata file: ${error}`,
|
|
56
|
+
severity: "error",
|
|
57
|
+
location: undefined,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Parse JSON
|
|
64
|
+
let parsed: unknown;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(content);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
error: [
|
|
71
|
+
{
|
|
72
|
+
code: "TSN9003",
|
|
73
|
+
message: `Invalid JSON in metadata file: ${error}`,
|
|
74
|
+
severity: "error",
|
|
75
|
+
location: undefined,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Validate structure
|
|
82
|
+
const validation = validateMetadataFile(parsed, filePath);
|
|
83
|
+
if (!validation.ok) {
|
|
84
|
+
return validation;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { ok: true, value: validation.value };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validate that parsed JSON matches MetadataFile schema.
|
|
92
|
+
*
|
|
93
|
+
* @param data - Parsed JSON data
|
|
94
|
+
* @param filePath - File path for error messages
|
|
95
|
+
* @returns Result containing validated metadata or diagnostics
|
|
96
|
+
*/
|
|
97
|
+
const validateMetadataFile = (
|
|
98
|
+
data: unknown,
|
|
99
|
+
filePath: string
|
|
100
|
+
): Result<MetadataFile, Diagnostic[]> => {
|
|
101
|
+
const diagnostics: Diagnostic[] = [];
|
|
102
|
+
|
|
103
|
+
if (typeof data !== "object" || data === null) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
error: [
|
|
107
|
+
{
|
|
108
|
+
code: "TSN9004",
|
|
109
|
+
message: `Metadata file must be an object, got ${typeof data}`,
|
|
110
|
+
severity: "error",
|
|
111
|
+
location: undefined,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const obj = data as Record<string, unknown>;
|
|
118
|
+
|
|
119
|
+
// Validate namespace
|
|
120
|
+
if (typeof obj.namespace !== "string") {
|
|
121
|
+
diagnostics.push({
|
|
122
|
+
code: "TSN9005",
|
|
123
|
+
message: `Missing or invalid 'namespace' field in ${path.basename(filePath)}`,
|
|
124
|
+
severity: "error",
|
|
125
|
+
location: undefined,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Validate contributingAssemblies
|
|
130
|
+
if (!Array.isArray(obj.contributingAssemblies)) {
|
|
131
|
+
diagnostics.push({
|
|
132
|
+
code: "TSN9006",
|
|
133
|
+
message: `Missing or invalid 'contributingAssemblies' field in ${path.basename(filePath)}`,
|
|
134
|
+
severity: "error",
|
|
135
|
+
location: undefined,
|
|
136
|
+
});
|
|
137
|
+
} else if (
|
|
138
|
+
!obj.contributingAssemblies.every((item) => typeof item === "string")
|
|
139
|
+
) {
|
|
140
|
+
diagnostics.push({
|
|
141
|
+
code: "TSN9007",
|
|
142
|
+
message: `All 'contributingAssemblies' must be strings in ${path.basename(filePath)}`,
|
|
143
|
+
severity: "error",
|
|
144
|
+
location: undefined,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Validate types array
|
|
149
|
+
if (!Array.isArray(obj.types)) {
|
|
150
|
+
diagnostics.push({
|
|
151
|
+
code: "TSN9008",
|
|
152
|
+
message: `Missing or invalid 'types' field in ${path.basename(filePath)}`,
|
|
153
|
+
severity: "error",
|
|
154
|
+
location: undefined,
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
// Validate each type
|
|
158
|
+
for (let i = 0; i < obj.types.length; i++) {
|
|
159
|
+
const typeValidation = validateTypeMetadata(obj.types[i], filePath, i);
|
|
160
|
+
if (!typeValidation.ok) {
|
|
161
|
+
diagnostics.push(...typeValidation.error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (diagnostics.length > 0) {
|
|
167
|
+
return { ok: false, error: diagnostics };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { ok: true, value: obj as MetadataFile };
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Validate a single TypeMetadata object.
|
|
175
|
+
*
|
|
176
|
+
* @param data - Type metadata to validate
|
|
177
|
+
* @param filePath - File path for error messages
|
|
178
|
+
* @param index - Type index in array
|
|
179
|
+
* @returns Result indicating validation success or errors
|
|
180
|
+
*/
|
|
181
|
+
const validateTypeMetadata = (
|
|
182
|
+
data: unknown,
|
|
183
|
+
filePath: string,
|
|
184
|
+
index: number
|
|
185
|
+
): Result<TypeMetadata, Diagnostic[]> => {
|
|
186
|
+
const diagnostics: Diagnostic[] = [];
|
|
187
|
+
const context = `type ${index} in ${path.basename(filePath)}`;
|
|
188
|
+
|
|
189
|
+
if (typeof data !== "object" || data === null) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
error: [
|
|
193
|
+
{
|
|
194
|
+
code: "TSN9009",
|
|
195
|
+
message: `Invalid ${context}: must be an object`,
|
|
196
|
+
severity: "error",
|
|
197
|
+
location: undefined,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const type = data as Record<string, unknown>;
|
|
204
|
+
|
|
205
|
+
// Validate required string fields
|
|
206
|
+
const requiredStringFields = ["clrName", "tsEmitName"];
|
|
207
|
+
for (const field of requiredStringFields) {
|
|
208
|
+
if (typeof type[field] !== "string") {
|
|
209
|
+
diagnostics.push({
|
|
210
|
+
code: "TSN9010",
|
|
211
|
+
message: `Invalid ${context}: missing or invalid '${field}'`,
|
|
212
|
+
severity: "error",
|
|
213
|
+
location: undefined,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Validate kind
|
|
219
|
+
const validKinds: TypeKind[] = [
|
|
220
|
+
"Class",
|
|
221
|
+
"Interface",
|
|
222
|
+
"Struct",
|
|
223
|
+
"Enum",
|
|
224
|
+
"Delegate",
|
|
225
|
+
];
|
|
226
|
+
if (!validKinds.includes(type.kind as TypeKind)) {
|
|
227
|
+
diagnostics.push({
|
|
228
|
+
code: "TSN9011",
|
|
229
|
+
message: `Invalid ${context}: 'kind' must be one of ${validKinds.join(", ")}`,
|
|
230
|
+
severity: "error",
|
|
231
|
+
location: undefined,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Validate accessibility
|
|
236
|
+
const validAccessibility: Accessibility[] = [
|
|
237
|
+
"Public",
|
|
238
|
+
"Internal",
|
|
239
|
+
"Protected",
|
|
240
|
+
"ProtectedInternal",
|
|
241
|
+
"PrivateProtected",
|
|
242
|
+
"Private",
|
|
243
|
+
];
|
|
244
|
+
if (!validAccessibility.includes(type.accessibility as Accessibility)) {
|
|
245
|
+
diagnostics.push({
|
|
246
|
+
code: "TSN9012",
|
|
247
|
+
message: `Invalid ${context}: 'accessibility' must be one of ${validAccessibility.join(", ")}`,
|
|
248
|
+
severity: "error",
|
|
249
|
+
location: undefined,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Validate required boolean fields
|
|
254
|
+
const requiredBooleanFields = ["isAbstract", "isSealed", "isStatic"];
|
|
255
|
+
for (const field of requiredBooleanFields) {
|
|
256
|
+
if (typeof type[field] !== "boolean") {
|
|
257
|
+
diagnostics.push({
|
|
258
|
+
code: "TSN9013",
|
|
259
|
+
message: `Invalid ${context}: '${field}' must be a boolean`,
|
|
260
|
+
severity: "error",
|
|
261
|
+
location: undefined,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Validate arity
|
|
267
|
+
if (typeof type.arity !== "number" || type.arity < 0) {
|
|
268
|
+
diagnostics.push({
|
|
269
|
+
code: "TSN9014",
|
|
270
|
+
message: `Invalid ${context}: 'arity' must be a non-negative number`,
|
|
271
|
+
severity: "error",
|
|
272
|
+
location: undefined,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Validate required array fields
|
|
277
|
+
const requiredArrayFields = [
|
|
278
|
+
"methods",
|
|
279
|
+
"properties",
|
|
280
|
+
"fields",
|
|
281
|
+
"events",
|
|
282
|
+
"constructors",
|
|
283
|
+
];
|
|
284
|
+
for (const field of requiredArrayFields) {
|
|
285
|
+
if (!Array.isArray(type[field])) {
|
|
286
|
+
diagnostics.push({
|
|
287
|
+
code: "TSN9015",
|
|
288
|
+
message: `Invalid ${context}: '${field}' must be an array`,
|
|
289
|
+
severity: "error",
|
|
290
|
+
location: undefined,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Note: We're doing basic structural validation here
|
|
296
|
+
// More detailed validation of nested objects (methods, properties, etc.)
|
|
297
|
+
// can be added in future iterations if needed
|
|
298
|
+
|
|
299
|
+
if (diagnostics.length > 0) {
|
|
300
|
+
return { ok: false, error: diagnostics };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return { ok: true, value: type as TypeMetadata };
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Load metadata from a directory containing .metadata.json files.
|
|
308
|
+
*
|
|
309
|
+
* @param directoryPath - Path to directory containing metadata files
|
|
310
|
+
* @returns Result containing array of loaded metadata files or diagnostics
|
|
311
|
+
*/
|
|
312
|
+
export const loadMetadataDirectory = (
|
|
313
|
+
directoryPath: string
|
|
314
|
+
): Result<MetadataFile[], Diagnostic[]> => {
|
|
315
|
+
if (!fs.existsSync(directoryPath)) {
|
|
316
|
+
return {
|
|
317
|
+
ok: false,
|
|
318
|
+
error: [
|
|
319
|
+
{
|
|
320
|
+
code: "TSN9016",
|
|
321
|
+
message: `Metadata directory not found: ${directoryPath}`,
|
|
322
|
+
severity: "error",
|
|
323
|
+
location: undefined,
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const stats = fs.statSync(directoryPath);
|
|
330
|
+
if (!stats.isDirectory()) {
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
error: [
|
|
334
|
+
{
|
|
335
|
+
code: "TSN9017",
|
|
336
|
+
message: `Not a directory: ${directoryPath}`,
|
|
337
|
+
severity: "error",
|
|
338
|
+
location: undefined,
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Find all .metadata.json files
|
|
345
|
+
const files = fs
|
|
346
|
+
.readdirSync(directoryPath)
|
|
347
|
+
.filter((file) => file.endsWith(".metadata.json"))
|
|
348
|
+
.map((file) => path.join(directoryPath, file));
|
|
349
|
+
|
|
350
|
+
if (files.length === 0) {
|
|
351
|
+
return {
|
|
352
|
+
ok: false,
|
|
353
|
+
error: [
|
|
354
|
+
{
|
|
355
|
+
code: "TSN9018",
|
|
356
|
+
message: `No .metadata.json files found in ${directoryPath}`,
|
|
357
|
+
severity: "warning",
|
|
358
|
+
location: undefined,
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Load each file
|
|
365
|
+
const metadataFiles: MetadataFile[] = [];
|
|
366
|
+
const diagnostics: Diagnostic[] = [];
|
|
367
|
+
|
|
368
|
+
for (const file of files) {
|
|
369
|
+
const result = loadMetadataFile(file);
|
|
370
|
+
if (result.ok) {
|
|
371
|
+
metadataFiles.push(result.value);
|
|
372
|
+
} else {
|
|
373
|
+
diagnostics.push(...result.error);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (diagnostics.length > 0) {
|
|
378
|
+
return { ok: false, error: diagnostics };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return { ok: true, value: metadataFiles };
|
|
382
|
+
};
|