@veewo/gitnexus 1.3.8 → 1.3.9
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 +5 -0
- package/dist/benchmark/runner.test.js +1 -1
- package/dist/benchmark/u2-e2e/analyze-parser.d.ts +22 -0
- package/dist/benchmark/u2-e2e/analyze-parser.js +89 -0
- package/dist/benchmark/u2-e2e/analyze-parser.test.d.ts +1 -0
- package/dist/benchmark/u2-e2e/analyze-parser.test.js +13 -0
- package/dist/benchmark/u2-e2e/characterlist-assetref.d.ts +19 -0
- package/dist/benchmark/u2-e2e/characterlist-assetref.js +80 -0
- package/dist/benchmark/u2-e2e/characterlist-assetref.test.d.ts +1 -0
- package/dist/benchmark/u2-e2e/characterlist-assetref.test.js +108 -0
- package/dist/benchmark/u2-e2e/config.d.ts +25 -0
- package/dist/benchmark/u2-e2e/config.js +86 -0
- package/dist/benchmark/u2-e2e/config.test.d.ts +1 -0
- package/dist/benchmark/u2-e2e/config.test.js +29 -0
- package/dist/benchmark/u2-e2e/metrics.d.ts +20 -0
- package/dist/benchmark/u2-e2e/metrics.js +34 -0
- package/dist/benchmark/u2-e2e/metrics.test.d.ts +1 -0
- package/dist/benchmark/u2-e2e/metrics.test.js +13 -0
- package/dist/benchmark/u2-e2e/neonspark-full-e2e.d.ts +33 -0
- package/dist/benchmark/u2-e2e/neonspark-full-e2e.js +439 -0
- package/dist/benchmark/u2-e2e/neonspark-full-e2e.test.d.ts +1 -0
- package/dist/benchmark/u2-e2e/neonspark-full-e2e.test.js +40 -0
- package/dist/benchmark/u2-e2e/report.d.ts +58 -0
- package/dist/benchmark/u2-e2e/report.js +130 -0
- package/dist/benchmark/u2-e2e/report.test.d.ts +1 -0
- package/dist/benchmark/u2-e2e/report.test.js +58 -0
- package/dist/benchmark/u2-e2e/retrieval-runner.d.ts +21 -0
- package/dist/benchmark/u2-e2e/retrieval-runner.js +166 -0
- package/dist/benchmark/u2-e2e/retrieval-runner.test.d.ts +1 -0
- package/dist/benchmark/u2-e2e/retrieval-runner.test.js +145 -0
- package/dist/benchmark/u2-performance-sampler.d.ts +33 -0
- package/dist/benchmark/u2-performance-sampler.js +178 -0
- package/dist/benchmark/u2-performance-sampler.test.d.ts +1 -0
- package/dist/benchmark/u2-performance-sampler.test.js +34 -0
- package/dist/cli/analyze-multi-scope-regression.test.js +10 -0
- package/dist/cli/analyze-summary.d.ts +7 -0
- package/dist/cli/analyze-summary.js +37 -0
- package/dist/cli/analyze-summary.test.d.ts +1 -0
- package/dist/cli/analyze-summary.test.js +58 -0
- package/dist/cli/analyze.js +11 -6
- package/dist/cli/benchmark-u2-e2e.d.ts +9 -0
- package/dist/cli/benchmark-u2-e2e.js +35 -0
- package/dist/cli/benchmark-u2-e2e.test.d.ts +1 -0
- package/dist/cli/benchmark-u2-e2e.test.js +7 -0
- package/dist/cli/index.js +20 -0
- package/dist/cli/tool.d.ts +3 -0
- package/dist/cli/tool.js +2 -0
- package/dist/cli/unity-bindings.d.ts +8 -0
- package/dist/cli/unity-bindings.js +33 -0
- package/dist/cli/unity-bindings.test.d.ts +1 -0
- package/dist/cli/unity-bindings.test.js +24 -0
- package/dist/core/graph/types.d.ts +1 -1
- package/dist/core/ingestion/pipeline.d.ts +2 -4
- package/dist/core/ingestion/pipeline.js +12 -0
- package/dist/core/ingestion/unity-resource-processor.d.ts +26 -0
- package/dist/core/ingestion/unity-resource-processor.js +363 -0
- package/dist/core/ingestion/unity-resource-processor.test.d.ts +1 -0
- package/dist/core/ingestion/unity-resource-processor.test.js +599 -0
- package/dist/core/kuzu/kuzu-adapter.d.ts +6 -0
- package/dist/core/kuzu/kuzu-adapter.js +18 -7
- package/dist/core/kuzu/schema.d.ts +2 -2
- package/dist/core/kuzu/schema.js +22 -1
- package/dist/core/kuzu/schema.test.d.ts +1 -0
- package/dist/core/kuzu/schema.test.js +17 -0
- package/dist/core/unity/meta-index.d.ts +5 -0
- package/dist/core/unity/meta-index.js +113 -0
- package/dist/core/unity/meta-index.test.d.ts +1 -0
- package/dist/core/unity/meta-index.test.js +11 -0
- package/dist/core/unity/options.d.ts +2 -0
- package/dist/core/unity/options.js +9 -0
- package/dist/core/unity/options.test.d.ts +1 -0
- package/dist/core/unity/options.test.js +10 -0
- package/dist/core/unity/override-merger.d.ts +27 -0
- package/dist/core/unity/override-merger.js +35 -0
- package/dist/core/unity/override-merger.test.d.ts +1 -0
- package/dist/core/unity/override-merger.test.js +47 -0
- package/dist/core/unity/resolver.d.ts +79 -0
- package/dist/core/unity/resolver.js +384 -0
- package/dist/core/unity/resolver.test.d.ts +1 -0
- package/dist/core/unity/resolver.test.js +244 -0
- package/dist/core/unity/resource-hit-scanner.d.ts +10 -0
- package/dist/core/unity/resource-hit-scanner.js +60 -0
- package/dist/core/unity/resource-hit-scanner.test.d.ts +1 -0
- package/dist/core/unity/resource-hit-scanner.test.js +20 -0
- package/dist/core/unity/scan-context.d.ts +23 -0
- package/dist/core/unity/scan-context.js +318 -0
- package/dist/core/unity/scan-context.test.d.ts +1 -0
- package/dist/core/unity/scan-context.test.js +118 -0
- package/dist/core/unity/serialized-type-index.d.ts +10 -0
- package/dist/core/unity/serialized-type-index.js +105 -0
- package/dist/core/unity/serialized-type-index.test.d.ts +1 -0
- package/dist/core/unity/serialized-type-index.test.js +34 -0
- package/dist/core/unity/u2-thresholds.test.d.ts +1 -0
- package/dist/core/unity/u2-thresholds.test.js +71 -0
- package/dist/core/unity/yaml-object-graph.d.ts +9 -0
- package/dist/core/unity/yaml-object-graph.js +92 -0
- package/dist/core/unity/yaml-object-graph.test.d.ts +1 -0
- package/dist/core/unity/yaml-object-graph.test.js +49 -0
- package/dist/mcp/local/local-backend.js +12 -1
- package/dist/mcp/local/unity-enrichment.d.ts +6 -0
- package/dist/mcp/local/unity-enrichment.js +91 -0
- package/dist/mcp/local/unity-enrichment.test.d.ts +1 -0
- package/dist/mcp/local/unity-enrichment.test.js +130 -0
- package/dist/mcp/tools.js +12 -0
- package/dist/types/pipeline.d.ts +7 -0
- package/dist/types/pipeline.js +2 -0
- package/hooks/check-release-path-hygiene.mjs +108 -0
- package/package.json +14 -7
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
export async function findGuidHits(repoRoot, guid, options = {}) {
|
|
5
|
+
const resourceFiles = await resolveResourceFiles(repoRoot, options.resourceFiles);
|
|
6
|
+
const hits = [];
|
|
7
|
+
for (const resourcePath of resourceFiles) {
|
|
8
|
+
const absolutePath = path.join(repoRoot, resourcePath);
|
|
9
|
+
let content = '';
|
|
10
|
+
try {
|
|
11
|
+
content = await fs.readFile(absolutePath, 'utf-8');
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error.code === 'ENOENT')
|
|
15
|
+
continue;
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
const lines = content.split(/\r?\n/);
|
|
19
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
20
|
+
if (!lines[index].includes(guid))
|
|
21
|
+
continue;
|
|
22
|
+
hits.push({
|
|
23
|
+
resourcePath: resourcePath.replace(/\\/g, '/'),
|
|
24
|
+
resourceType: inferResourceType(resourcePath),
|
|
25
|
+
line: index + 1,
|
|
26
|
+
lineText: lines[index],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return hits;
|
|
31
|
+
}
|
|
32
|
+
async function resolveResourceFiles(repoRoot, scopedResourceFiles) {
|
|
33
|
+
if (!scopedResourceFiles || scopedResourceFiles.length === 0) {
|
|
34
|
+
return (await glob(['**/*.prefab', '**/*.unity', '**/*.asset'], {
|
|
35
|
+
cwd: repoRoot,
|
|
36
|
+
nodir: true,
|
|
37
|
+
dot: false,
|
|
38
|
+
})).sort((left, right) => left.localeCompare(right));
|
|
39
|
+
}
|
|
40
|
+
const normalized = scopedResourceFiles
|
|
41
|
+
.filter((value) => value.endsWith('.prefab') || value.endsWith('.unity') || value.endsWith('.asset'))
|
|
42
|
+
.map((value) => normalizeRelativePath(repoRoot, value))
|
|
43
|
+
.filter((value) => value !== null)
|
|
44
|
+
.sort((left, right) => left.localeCompare(right));
|
|
45
|
+
return [...new Set(normalized)];
|
|
46
|
+
}
|
|
47
|
+
function normalizeRelativePath(repoRoot, filePath) {
|
|
48
|
+
const relativePath = path.isAbsolute(filePath) ? path.relative(repoRoot, filePath) : filePath;
|
|
49
|
+
const normalized = relativePath.replace(/\\/g, '/');
|
|
50
|
+
if (normalized.startsWith('../'))
|
|
51
|
+
return null;
|
|
52
|
+
return normalized;
|
|
53
|
+
}
|
|
54
|
+
function inferResourceType(resourcePath) {
|
|
55
|
+
if (resourcePath.endsWith('.prefab'))
|
|
56
|
+
return 'prefab';
|
|
57
|
+
if (resourcePath.endsWith('.asset'))
|
|
58
|
+
return 'asset';
|
|
59
|
+
return 'scene';
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { findGuidHits } from './resource-hit-scanner.js';
|
|
6
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const fixtureRoot = path.resolve(here, '../../../src/core/unity/__fixtures__/mini-unity');
|
|
8
|
+
test('findGuidHits returns resource hits for matching MonoBehaviour scripts', async () => {
|
|
9
|
+
const hits = await findGuidHits(fixtureRoot, 'a6d481d58c0b4f646b7106ceaf633d6e');
|
|
10
|
+
assert.equal(hits.length, 1);
|
|
11
|
+
assert.equal(hits[0].resourceType, 'scene');
|
|
12
|
+
assert.equal(hits[0].resourcePath, 'Assets/Scene/Global.unity');
|
|
13
|
+
assert.equal(hits[0].line, 9);
|
|
14
|
+
});
|
|
15
|
+
test('findGuidHits includes ScriptableObject .asset resources', async () => {
|
|
16
|
+
const hits = await findGuidHits(fixtureRoot, 'abababababababababababababababab');
|
|
17
|
+
assert.equal(hits.length, 1);
|
|
18
|
+
assert.equal(hits[0].resourceType, 'asset');
|
|
19
|
+
assert.equal(hits[0].resourcePath, 'Assets/Config/U2ScriptableConfig.asset');
|
|
20
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { UnityResourceGuidHit } from './resource-hit-scanner.js';
|
|
2
|
+
import type { UnityObjectBlock } from './yaml-object-graph.js';
|
|
3
|
+
export interface BuildScanContextInput {
|
|
4
|
+
repoRoot: string;
|
|
5
|
+
scopedPaths?: string[];
|
|
6
|
+
symbolDeclarations?: UnitySymbolDeclaration[];
|
|
7
|
+
}
|
|
8
|
+
export interface UnitySymbolDeclaration {
|
|
9
|
+
symbol: string;
|
|
10
|
+
scriptPath: string;
|
|
11
|
+
}
|
|
12
|
+
export interface UnityScanContext {
|
|
13
|
+
symbolToScriptPaths: Map<string, string[]>;
|
|
14
|
+
symbolToCanonicalScriptPath: Map<string, string>;
|
|
15
|
+
symbolToScriptPath: Map<string, string>;
|
|
16
|
+
scriptPathToGuid: Map<string, string>;
|
|
17
|
+
guidToResourceHits: Map<string, UnityResourceGuidHit[]>;
|
|
18
|
+
serializableSymbols: Set<string>;
|
|
19
|
+
hostFieldTypeHints: Map<string, Map<string, string>>;
|
|
20
|
+
assetGuidToPath?: Map<string, string>;
|
|
21
|
+
resourceDocCache: Map<string, UnityObjectBlock[]>;
|
|
22
|
+
}
|
|
23
|
+
export declare function buildUnityScanContext(input: BuildScanContextInput): Promise<UnityScanContext>;
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
import { buildAssetMetaIndex, buildMetaIndex } from './meta-index.js';
|
|
5
|
+
import { buildSerializableTypeIndexFromSources } from './serialized-type-index.js';
|
|
6
|
+
const DECLARATION_PATTERN = /\b(?:class|struct|interface)\s+([A-Za-z_][A-Za-z0-9_]*)\b/g;
|
|
7
|
+
const GUID_IN_LINE_PATTERN = /\bguid:\s*([0-9a-f]{32})\b/gi;
|
|
8
|
+
const RESOURCE_HIT_SCAN_CONCURRENCY = 16;
|
|
9
|
+
export async function buildUnityScanContext(input) {
|
|
10
|
+
const scriptFiles = input.symbolDeclarations && input.symbolDeclarations.length > 0
|
|
11
|
+
? resolveScriptFilesFromSymbolDeclarations(input.repoRoot, input.symbolDeclarations, input.scopedPaths)
|
|
12
|
+
: await resolveScriptFiles(input.repoRoot, input.scopedPaths);
|
|
13
|
+
const symbolToScriptPaths = input.symbolDeclarations && input.symbolDeclarations.length > 0
|
|
14
|
+
? buildSymbolScriptPathIndexFromDeclarations(input.repoRoot, input.symbolDeclarations, input.scopedPaths)
|
|
15
|
+
: await buildSymbolScriptPathIndex(input.repoRoot, scriptFiles);
|
|
16
|
+
const scriptSources = await loadScriptSources(input.repoRoot, scriptFiles);
|
|
17
|
+
const serializableTypeIndex = buildSerializableTypeIndexFromSources(scriptSources);
|
|
18
|
+
const metaFiles = scriptFiles.map((scriptPath) => `${scriptPath}.meta`);
|
|
19
|
+
const guidToScriptPath = await buildMetaIndex(input.repoRoot, { metaFiles });
|
|
20
|
+
const scriptPathToGuid = new Map();
|
|
21
|
+
for (const [guid, scriptPath] of guidToScriptPath.entries()) {
|
|
22
|
+
scriptPathToGuid.set(normalizeSlashes(scriptPath), guid);
|
|
23
|
+
}
|
|
24
|
+
const resourceFiles = await resolveResourceFiles(input.repoRoot, input.scopedPaths);
|
|
25
|
+
const guidToResourceHits = await buildGuidHitIndex(input.repoRoot, scriptPathToGuid, resourceFiles);
|
|
26
|
+
const assetMetaFiles = resolveAssetMetaFiles(input.repoRoot, input.scopedPaths, scriptFiles, resourceFiles);
|
|
27
|
+
const assetGuidToPath = await buildAssetMetaIndex(input.repoRoot, { metaFiles: assetMetaFiles });
|
|
28
|
+
const symbolToCanonicalScriptPath = buildCanonicalScriptPathIndex(symbolToScriptPaths, scriptPathToGuid, guidToResourceHits);
|
|
29
|
+
const symbolToScriptPath = new Map(symbolToCanonicalScriptPath);
|
|
30
|
+
return {
|
|
31
|
+
symbolToScriptPaths,
|
|
32
|
+
symbolToCanonicalScriptPath,
|
|
33
|
+
symbolToScriptPath,
|
|
34
|
+
scriptPathToGuid,
|
|
35
|
+
guidToResourceHits,
|
|
36
|
+
serializableSymbols: serializableTypeIndex.serializableSymbols,
|
|
37
|
+
hostFieldTypeHints: serializableTypeIndex.hostFieldTypeHints,
|
|
38
|
+
assetGuidToPath,
|
|
39
|
+
resourceDocCache: new Map(),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async function loadScriptSources(repoRoot, scriptFiles) {
|
|
43
|
+
const sources = [];
|
|
44
|
+
for (const scriptPath of scriptFiles) {
|
|
45
|
+
const normalizedPath = normalizeSlashes(scriptPath);
|
|
46
|
+
try {
|
|
47
|
+
const content = await fs.readFile(path.join(repoRoot, normalizedPath), 'utf-8');
|
|
48
|
+
sources.push({ filePath: normalizedPath, content });
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code === 'ENOENT')
|
|
52
|
+
continue;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return sources;
|
|
57
|
+
}
|
|
58
|
+
async function buildSymbolScriptPathIndex(repoRoot, scriptFiles) {
|
|
59
|
+
const candidates = new Map();
|
|
60
|
+
for (const scriptPath of scriptFiles) {
|
|
61
|
+
const normalizedPath = normalizeSlashes(scriptPath);
|
|
62
|
+
addSymbolCandidate(candidates, path.basename(normalizedPath, '.cs'), normalizedPath);
|
|
63
|
+
const absolutePath = path.join(repoRoot, normalizedPath);
|
|
64
|
+
let content = '';
|
|
65
|
+
try {
|
|
66
|
+
content = await fs.readFile(absolutePath, 'utf-8');
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code === 'ENOENT')
|
|
70
|
+
continue;
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
DECLARATION_PATTERN.lastIndex = 0;
|
|
74
|
+
let match = DECLARATION_PATTERN.exec(content);
|
|
75
|
+
while (match) {
|
|
76
|
+
addSymbolCandidate(candidates, match[1], normalizedPath);
|
|
77
|
+
match = DECLARATION_PATTERN.exec(content);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const symbolToScriptPaths = new Map();
|
|
81
|
+
for (const [symbol, paths] of candidates.entries()) {
|
|
82
|
+
symbolToScriptPaths.set(symbol, [...paths].sort((left, right) => left.localeCompare(right)));
|
|
83
|
+
}
|
|
84
|
+
return symbolToScriptPaths;
|
|
85
|
+
}
|
|
86
|
+
async function buildGuidHitIndex(repoRoot, scriptPathToGuid, resourceFiles) {
|
|
87
|
+
if (scriptPathToGuid.size === 0 || resourceFiles.length === 0) {
|
|
88
|
+
return new Map();
|
|
89
|
+
}
|
|
90
|
+
const guidLookup = new Map();
|
|
91
|
+
for (const guid of scriptPathToGuid.values()) {
|
|
92
|
+
guidLookup.set(guid.toLowerCase(), guid);
|
|
93
|
+
}
|
|
94
|
+
const perResourceHits = await mapWithConcurrency(resourceFiles, RESOURCE_HIT_SCAN_CONCURRENCY, async (resourcePathRaw) => {
|
|
95
|
+
const resourcePath = normalizeSlashes(resourcePathRaw);
|
|
96
|
+
const absolutePath = path.join(repoRoot, resourcePath);
|
|
97
|
+
let content = '';
|
|
98
|
+
try {
|
|
99
|
+
content = await fs.readFile(absolutePath, 'utf-8');
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
const code = error.code;
|
|
103
|
+
if (code === 'ENOENT' || code === 'EISDIR') {
|
|
104
|
+
return new Map();
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
const resourceType = inferResourceType(resourcePath);
|
|
109
|
+
const lines = content.split(/\r?\n/);
|
|
110
|
+
const hits = new Map();
|
|
111
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
112
|
+
const line = lines[index];
|
|
113
|
+
const seenCanonical = new Set();
|
|
114
|
+
GUID_IN_LINE_PATTERN.lastIndex = 0;
|
|
115
|
+
let match = GUID_IN_LINE_PATTERN.exec(line);
|
|
116
|
+
while (match) {
|
|
117
|
+
const canonicalGuid = guidLookup.get(match[1].toLowerCase());
|
|
118
|
+
if (canonicalGuid && !seenCanonical.has(canonicalGuid)) {
|
|
119
|
+
seenCanonical.add(canonicalGuid);
|
|
120
|
+
const existing = hits.get(canonicalGuid) || [];
|
|
121
|
+
existing.push({
|
|
122
|
+
resourcePath,
|
|
123
|
+
resourceType,
|
|
124
|
+
line: index + 1,
|
|
125
|
+
lineText: line,
|
|
126
|
+
});
|
|
127
|
+
hits.set(canonicalGuid, existing);
|
|
128
|
+
}
|
|
129
|
+
match = GUID_IN_LINE_PATTERN.exec(line);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return hits;
|
|
133
|
+
});
|
|
134
|
+
const guidToResourceHits = new Map();
|
|
135
|
+
for (const hitMap of perResourceHits) {
|
|
136
|
+
for (const [guid, hits] of hitMap.entries()) {
|
|
137
|
+
const existing = guidToResourceHits.get(guid) || [];
|
|
138
|
+
existing.push(...hits);
|
|
139
|
+
guidToResourceHits.set(guid, existing);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return guidToResourceHits;
|
|
143
|
+
}
|
|
144
|
+
async function resolveScriptFiles(repoRoot, scopedPaths) {
|
|
145
|
+
if (!scopedPaths || scopedPaths.length === 0) {
|
|
146
|
+
return (await glob('**/*.cs', {
|
|
147
|
+
cwd: repoRoot,
|
|
148
|
+
nodir: true,
|
|
149
|
+
dot: false,
|
|
150
|
+
})).sort((left, right) => left.localeCompare(right));
|
|
151
|
+
}
|
|
152
|
+
const scopedScripts = scopedPaths
|
|
153
|
+
.filter((value) => value.endsWith('.cs'))
|
|
154
|
+
.map((value) => normalizeRelativePath(repoRoot, value))
|
|
155
|
+
.filter((value) => value !== null)
|
|
156
|
+
.sort((left, right) => left.localeCompare(right));
|
|
157
|
+
return [...new Set(scopedScripts)];
|
|
158
|
+
}
|
|
159
|
+
async function resolveResourceFiles(repoRoot, scopedPaths) {
|
|
160
|
+
if (!scopedPaths || scopedPaths.length === 0) {
|
|
161
|
+
return (await glob(['**/*.prefab', '**/*.unity', '**/*.asset'], {
|
|
162
|
+
cwd: repoRoot,
|
|
163
|
+
nodir: true,
|
|
164
|
+
dot: false,
|
|
165
|
+
})).sort((left, right) => left.localeCompare(right));
|
|
166
|
+
}
|
|
167
|
+
const scopedResources = scopedPaths
|
|
168
|
+
.filter((value) => value.endsWith('.prefab') || value.endsWith('.unity') || value.endsWith('.asset'))
|
|
169
|
+
.map((value) => normalizeRelativePath(repoRoot, value))
|
|
170
|
+
.filter((value) => value !== null)
|
|
171
|
+
.sort((left, right) => left.localeCompare(right));
|
|
172
|
+
return [...new Set(scopedResources)];
|
|
173
|
+
}
|
|
174
|
+
function addSymbolCandidate(candidates, symbol, scriptPath) {
|
|
175
|
+
const existing = candidates.get(symbol) || new Set();
|
|
176
|
+
existing.add(scriptPath);
|
|
177
|
+
candidates.set(symbol, existing);
|
|
178
|
+
}
|
|
179
|
+
function buildSymbolScriptPathIndexFromDeclarations(repoRoot, declarations, scopedPaths) {
|
|
180
|
+
const candidates = new Map();
|
|
181
|
+
const allowedScriptPaths = resolveScopedScriptAllowlist(repoRoot, scopedPaths);
|
|
182
|
+
for (const declaration of declarations) {
|
|
183
|
+
const symbol = String(declaration.symbol || '').trim();
|
|
184
|
+
if (!symbol)
|
|
185
|
+
continue;
|
|
186
|
+
const scriptPath = normalizeRelativePath(repoRoot, declaration.scriptPath);
|
|
187
|
+
if (!scriptPath)
|
|
188
|
+
continue;
|
|
189
|
+
if (allowedScriptPaths && !allowedScriptPaths.has(scriptPath))
|
|
190
|
+
continue;
|
|
191
|
+
addSymbolCandidate(candidates, symbol, scriptPath);
|
|
192
|
+
}
|
|
193
|
+
const symbolToScriptPaths = new Map();
|
|
194
|
+
for (const [symbol, paths] of candidates.entries()) {
|
|
195
|
+
symbolToScriptPaths.set(symbol, [...paths].sort((left, right) => left.localeCompare(right)));
|
|
196
|
+
}
|
|
197
|
+
return symbolToScriptPaths;
|
|
198
|
+
}
|
|
199
|
+
function buildCanonicalScriptPathIndex(symbolToScriptPaths, scriptPathToGuid, guidToResourceHits) {
|
|
200
|
+
const canonical = new Map();
|
|
201
|
+
for (const [symbol, scriptPaths] of symbolToScriptPaths.entries()) {
|
|
202
|
+
if (scriptPaths.length === 0)
|
|
203
|
+
continue;
|
|
204
|
+
const selected = selectCanonicalScriptPath(symbol, scriptPaths, scriptPathToGuid, guidToResourceHits);
|
|
205
|
+
if (selected) {
|
|
206
|
+
canonical.set(symbol, selected);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return canonical;
|
|
210
|
+
}
|
|
211
|
+
function selectCanonicalScriptPath(symbol, scriptPaths, scriptPathToGuid, guidToResourceHits) {
|
|
212
|
+
const uniquePaths = [...new Set(scriptPaths)].sort((left, right) => left.localeCompare(right));
|
|
213
|
+
if (uniquePaths.length === 0)
|
|
214
|
+
return null;
|
|
215
|
+
const symbolBaseName = `${symbol}.cs`.toLowerCase();
|
|
216
|
+
const symbolPrefix = `${symbol.toLowerCase()}.`;
|
|
217
|
+
const scored = uniquePaths.map((scriptPath) => {
|
|
218
|
+
const baseName = path.basename(scriptPath).toLowerCase();
|
|
219
|
+
const exactScore = baseName === symbolBaseName ? 0 : 1;
|
|
220
|
+
const generatedScore = baseName.endsWith('.generated.cs') ? 1 : 0;
|
|
221
|
+
const suffixScore = baseName.startsWith(symbolPrefix) && baseName !== symbolBaseName ? 1 : 0;
|
|
222
|
+
const guid = scriptPathToGuid.get(scriptPath);
|
|
223
|
+
const hitCount = guid ? (guidToResourceHits.get(guid)?.length || 0) : 0;
|
|
224
|
+
return {
|
|
225
|
+
scriptPath,
|
|
226
|
+
exactScore,
|
|
227
|
+
generatedScore,
|
|
228
|
+
suffixScore,
|
|
229
|
+
hitCount,
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
scored.sort((left, right) => {
|
|
233
|
+
if (left.exactScore !== right.exactScore)
|
|
234
|
+
return left.exactScore - right.exactScore;
|
|
235
|
+
if (left.generatedScore !== right.generatedScore)
|
|
236
|
+
return left.generatedScore - right.generatedScore;
|
|
237
|
+
if (left.suffixScore !== right.suffixScore)
|
|
238
|
+
return left.suffixScore - right.suffixScore;
|
|
239
|
+
if (left.hitCount !== right.hitCount)
|
|
240
|
+
return right.hitCount - left.hitCount;
|
|
241
|
+
return left.scriptPath.localeCompare(right.scriptPath);
|
|
242
|
+
});
|
|
243
|
+
return scored[0].scriptPath;
|
|
244
|
+
}
|
|
245
|
+
function resolveScriptFilesFromSymbolDeclarations(repoRoot, declarations, scopedPaths) {
|
|
246
|
+
const allowedScriptPaths = resolveScopedScriptAllowlist(repoRoot, scopedPaths);
|
|
247
|
+
const scriptFiles = declarations
|
|
248
|
+
.map((declaration) => normalizeRelativePath(repoRoot, declaration.scriptPath))
|
|
249
|
+
.filter((value) => value !== null)
|
|
250
|
+
.filter((value) => !allowedScriptPaths || allowedScriptPaths.has(value))
|
|
251
|
+
.sort((left, right) => left.localeCompare(right));
|
|
252
|
+
return [...new Set(scriptFiles)];
|
|
253
|
+
}
|
|
254
|
+
function resolveScopedScriptAllowlist(repoRoot, scopedPaths) {
|
|
255
|
+
if (!scopedPaths || scopedPaths.length === 0)
|
|
256
|
+
return null;
|
|
257
|
+
const allowlist = new Set(scopedPaths
|
|
258
|
+
.filter((value) => value.endsWith('.cs'))
|
|
259
|
+
.map((value) => normalizeRelativePath(repoRoot, value))
|
|
260
|
+
.filter((value) => value !== null));
|
|
261
|
+
return allowlist.size > 0 ? allowlist : null;
|
|
262
|
+
}
|
|
263
|
+
function normalizeRelativePath(repoRoot, filePath) {
|
|
264
|
+
const relativePath = path.isAbsolute(filePath) ? path.relative(repoRoot, filePath) : filePath;
|
|
265
|
+
const normalized = normalizeSlashes(relativePath);
|
|
266
|
+
if (normalized.startsWith('../'))
|
|
267
|
+
return null;
|
|
268
|
+
return normalized;
|
|
269
|
+
}
|
|
270
|
+
function normalizeSlashes(filePath) {
|
|
271
|
+
return filePath.replace(/\\/g, '/');
|
|
272
|
+
}
|
|
273
|
+
function inferResourceType(resourcePath) {
|
|
274
|
+
if (resourcePath.endsWith('.prefab'))
|
|
275
|
+
return 'prefab';
|
|
276
|
+
if (resourcePath.endsWith('.asset'))
|
|
277
|
+
return 'asset';
|
|
278
|
+
return 'scene';
|
|
279
|
+
}
|
|
280
|
+
function resolveAssetMetaFiles(repoRoot, scopedPaths, scriptFiles, resourceFiles) {
|
|
281
|
+
if (scopedPaths && scopedPaths.length > 0) {
|
|
282
|
+
const scopedMeta = new Set();
|
|
283
|
+
for (const entry of scopedPaths) {
|
|
284
|
+
const normalized = normalizeRelativePath(repoRoot, entry);
|
|
285
|
+
if (!normalized)
|
|
286
|
+
continue;
|
|
287
|
+
if (normalized.endsWith('.meta')) {
|
|
288
|
+
scopedMeta.add(normalized);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
scopedMeta.add(`${normalized}.meta`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return [...scopedMeta].sort((left, right) => left.localeCompare(right));
|
|
295
|
+
}
|
|
296
|
+
const inferredMeta = new Set();
|
|
297
|
+
for (const scriptPath of scriptFiles)
|
|
298
|
+
inferredMeta.add(`${scriptPath}.meta`);
|
|
299
|
+
for (const resourcePath of resourceFiles)
|
|
300
|
+
inferredMeta.add(`${resourcePath}.meta`);
|
|
301
|
+
return [...inferredMeta].sort((left, right) => left.localeCompare(right));
|
|
302
|
+
}
|
|
303
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
304
|
+
const safeConcurrency = Math.max(1, Math.min(concurrency, items.length || 1));
|
|
305
|
+
const results = new Array(items.length);
|
|
306
|
+
let cursor = 0;
|
|
307
|
+
const workers = Array.from({ length: safeConcurrency }, async () => {
|
|
308
|
+
while (true) {
|
|
309
|
+
const index = cursor;
|
|
310
|
+
cursor += 1;
|
|
311
|
+
if (index >= items.length)
|
|
312
|
+
break;
|
|
313
|
+
results[index] = await mapper(items[index], index);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
await Promise.all(workers);
|
|
317
|
+
return results;
|
|
318
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { buildUnityScanContext } from './scan-context.js';
|
|
8
|
+
import { resolveUnityBindings } from './resolver.js';
|
|
9
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const fixtureRoot = path.resolve(here, '../../../src/core/unity/__fixtures__/mini-unity');
|
|
11
|
+
test('buildUnityScanContext builds symbol/guid/hit indexes once from fixture', async () => {
|
|
12
|
+
const context = await buildUnityScanContext({ repoRoot: fixtureRoot });
|
|
13
|
+
assert.ok(context.symbolToScriptPath.has('MainUIManager'));
|
|
14
|
+
assert.ok(context.scriptPathToGuid.size > 0);
|
|
15
|
+
assert.ok(context.guidToResourceHits.size > 0);
|
|
16
|
+
});
|
|
17
|
+
test('buildUnityScanContext exposes reusable resourceDocCache for repeated resolves', async () => {
|
|
18
|
+
const context = await buildUnityScanContext({ repoRoot: fixtureRoot });
|
|
19
|
+
assert.equal(context.resourceDocCache.size, 0);
|
|
20
|
+
await resolveUnityBindings({ repoRoot: fixtureRoot, symbol: 'MainUIManager', scanContext: context });
|
|
21
|
+
const cacheSizeAfterFirst = context.resourceDocCache.size;
|
|
22
|
+
await resolveUnityBindings({ repoRoot: fixtureRoot, symbol: 'MainUIManager', scanContext: context });
|
|
23
|
+
assert.equal(context.resourceDocCache.size, cacheSizeAfterFirst);
|
|
24
|
+
assert.ok(cacheSizeAfterFirst > 0);
|
|
25
|
+
});
|
|
26
|
+
test('buildUnityScanContext accepts symbol declarations as hint source', async () => {
|
|
27
|
+
const context = await buildUnityScanContext({
|
|
28
|
+
repoRoot: fixtureRoot,
|
|
29
|
+
scopedPaths: ['Assets/Scene/MainUIManager.unity'],
|
|
30
|
+
symbolDeclarations: [
|
|
31
|
+
{ symbol: 'HintOnly', scriptPath: 'Assets/Scripts/HintOnly.cs' },
|
|
32
|
+
{ symbol: 'MainUIManager', scriptPath: 'Assets/Scripts/MainUIManager.cs' },
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
assert.equal(context.symbolToScriptPath.get('HintOnly'), 'Assets/Scripts/HintOnly.cs');
|
|
36
|
+
assert.equal(context.symbolToScriptPath.get('MainUIManager'), 'Assets/Scripts/MainUIManager.cs');
|
|
37
|
+
});
|
|
38
|
+
test('buildUnityScanContext skips resource scanning when there are no script guids', async () => {
|
|
39
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-scancontext-'));
|
|
40
|
+
const badResourceDir = path.join(tempRoot, 'Assets/Scene/Broken.unity');
|
|
41
|
+
await fs.mkdir(badResourceDir, { recursive: true });
|
|
42
|
+
try {
|
|
43
|
+
const context = await buildUnityScanContext({
|
|
44
|
+
repoRoot: tempRoot,
|
|
45
|
+
scopedPaths: ['Assets/Scene/Broken.unity'],
|
|
46
|
+
});
|
|
47
|
+
assert.equal(context.scriptPathToGuid.size, 0);
|
|
48
|
+
assert.equal(context.guidToResourceHits.size, 0);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
test('buildUnityScanContext indexes scoped asset meta files for guid->path resolution', async () => {
|
|
55
|
+
const context = await buildUnityScanContext({
|
|
56
|
+
repoRoot: fixtureRoot,
|
|
57
|
+
scopedPaths: [
|
|
58
|
+
'Assets/Scripts/MainUIManager.cs',
|
|
59
|
+
'Assets/Scripts/MainUIManager.cs.meta',
|
|
60
|
+
'Assets/Scene/MainUIManager.unity',
|
|
61
|
+
'Assets/Config/MainUIDocument.asset.meta',
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
assert.equal(context.assetGuidToPath?.get('44444444444444444444444444444444'), 'Assets/Config/MainUIDocument.asset');
|
|
65
|
+
});
|
|
66
|
+
test('buildUnityScanContext selects canonical script for duplicated symbol declarations', async () => {
|
|
67
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-canonical-'));
|
|
68
|
+
const scriptsDir = path.join(tempRoot, 'Assets/Scripts');
|
|
69
|
+
const sceneDir = path.join(tempRoot, 'Assets/Scene');
|
|
70
|
+
await fs.mkdir(scriptsDir, { recursive: true });
|
|
71
|
+
await fs.mkdir(sceneDir, { recursive: true });
|
|
72
|
+
try {
|
|
73
|
+
await fs.writeFile(path.join(scriptsDir, 'PlayerActor.cs'), 'public partial class PlayerActor {}', 'utf-8');
|
|
74
|
+
await fs.writeFile(path.join(scriptsDir, 'PlayerActor.Visual.cs'), 'public partial class PlayerActor {}', 'utf-8');
|
|
75
|
+
await fs.writeFile(path.join(scriptsDir, 'PlayerActor.cs.meta'), 'guid: 11111111111111111111111111111111\n', 'utf-8');
|
|
76
|
+
await fs.writeFile(path.join(scriptsDir, 'PlayerActor.Visual.cs.meta'), 'guid: 22222222222222222222222222222222\n', 'utf-8');
|
|
77
|
+
await fs.writeFile(path.join(sceneDir, 'Test.unity'), '--- !u!1 &1\nguid: 11111111111111111111111111111111\n', 'utf-8');
|
|
78
|
+
const context = await buildUnityScanContext({
|
|
79
|
+
repoRoot: tempRoot,
|
|
80
|
+
symbolDeclarations: [
|
|
81
|
+
{ symbol: 'PlayerActor', scriptPath: 'Assets/Scripts/PlayerActor.cs' },
|
|
82
|
+
{ symbol: 'PlayerActor', scriptPath: 'Assets/Scripts/PlayerActor.Visual.cs' },
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
assert.deepEqual(context.symbolToScriptPaths.get('PlayerActor'), [
|
|
86
|
+
'Assets/Scripts/PlayerActor.cs',
|
|
87
|
+
'Assets/Scripts/PlayerActor.Visual.cs',
|
|
88
|
+
]);
|
|
89
|
+
assert.equal(context.symbolToCanonicalScriptPath.get('PlayerActor'), 'Assets/Scripts/PlayerActor.cs');
|
|
90
|
+
assert.equal(context.symbolToScriptPath.get('PlayerActor'), 'Assets/Scripts/PlayerActor.cs');
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
test('buildUnityScanContext exposes serializable symbol index and host field type hints', async () => {
|
|
97
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-serializable-scancontext-'));
|
|
98
|
+
const scriptsDir = path.join(tempRoot, 'Assets/Scripts');
|
|
99
|
+
await fs.mkdir(scriptsDir, { recursive: true });
|
|
100
|
+
try {
|
|
101
|
+
await fs.writeFile(path.join(scriptsDir, 'AssetRef.cs'), `
|
|
102
|
+
[System.Serializable]
|
|
103
|
+
public class AssetRef { public string guid; }
|
|
104
|
+
`, 'utf-8');
|
|
105
|
+
await fs.writeFile(path.join(scriptsDir, 'InventoryConfig.cs'), `
|
|
106
|
+
using UnityEngine;
|
|
107
|
+
public class InventoryConfig : ScriptableObject {
|
|
108
|
+
public AssetRef icon;
|
|
109
|
+
}
|
|
110
|
+
`, 'utf-8');
|
|
111
|
+
const context = await buildUnityScanContext({ repoRoot: tempRoot });
|
|
112
|
+
assert.equal(context.serializableSymbols.has('AssetRef'), true);
|
|
113
|
+
assert.equal(context.hostFieldTypeHints.get('InventoryConfig')?.get('icon'), 'AssetRef');
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface SerializableTypeIndex {
|
|
2
|
+
serializableSymbols: Set<string>;
|
|
3
|
+
hostFieldTypeHints: Map<string, Map<string, string>>;
|
|
4
|
+
}
|
|
5
|
+
interface SourceFile {
|
|
6
|
+
filePath: string;
|
|
7
|
+
content: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function buildSerializableTypeIndexFromSources(sources: SourceFile[]): SerializableTypeIndex;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const SERIALIZABLE_DECLARATION_PATTERN = /(?:\[[^\]]*\bSerializable\b[^\]]*\]\s*)+(?:(?:public|private|protected|internal|static|sealed|abstract|partial)\s+)*(?:class|struct)\s+([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
2
|
+
const CLASS_DECLARATION_PATTERN = /\bclass\s+([A-Za-z_][A-Za-z0-9_]*)[^{]*\{/g;
|
|
3
|
+
const FIELD_DECLARATION_PATTERN = /(?:\[[^\]]+\]\s*)*(?:(?:public|private|protected|internal|static|readonly|volatile|new|sealed|virtual|override|unsafe)\s+)*([A-Za-z_][A-Za-z0-9_<>\[\],\.\?\s]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:=[^;]*)?;/g;
|
|
4
|
+
export function buildSerializableTypeIndexFromSources(sources) {
|
|
5
|
+
const serializableSymbols = new Set();
|
|
6
|
+
for (const source of sources) {
|
|
7
|
+
SERIALIZABLE_DECLARATION_PATTERN.lastIndex = 0;
|
|
8
|
+
let match = SERIALIZABLE_DECLARATION_PATTERN.exec(source.content);
|
|
9
|
+
while (match) {
|
|
10
|
+
serializableSymbols.add(match[1]);
|
|
11
|
+
match = SERIALIZABLE_DECLARATION_PATTERN.exec(source.content);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const hostFieldTypeHints = new Map();
|
|
15
|
+
for (const source of sources) {
|
|
16
|
+
const classBodies = extractClassBodies(source.content);
|
|
17
|
+
for (const classBody of classBodies) {
|
|
18
|
+
const fieldHints = extractHostFieldHints(classBody.body, serializableSymbols);
|
|
19
|
+
if (fieldHints.size > 0) {
|
|
20
|
+
hostFieldTypeHints.set(classBody.name, fieldHints);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { serializableSymbols, hostFieldTypeHints };
|
|
25
|
+
}
|
|
26
|
+
function extractClassBodies(content) {
|
|
27
|
+
const result = [];
|
|
28
|
+
CLASS_DECLARATION_PATTERN.lastIndex = 0;
|
|
29
|
+
let match = CLASS_DECLARATION_PATTERN.exec(content);
|
|
30
|
+
while (match) {
|
|
31
|
+
const className = match[1];
|
|
32
|
+
const openBraceIndex = CLASS_DECLARATION_PATTERN.lastIndex - 1;
|
|
33
|
+
const closeBraceIndex = findMatchingBrace(content, openBraceIndex);
|
|
34
|
+
if (closeBraceIndex !== -1) {
|
|
35
|
+
result.push({
|
|
36
|
+
name: className,
|
|
37
|
+
body: content.slice(openBraceIndex + 1, closeBraceIndex),
|
|
38
|
+
});
|
|
39
|
+
CLASS_DECLARATION_PATTERN.lastIndex = closeBraceIndex + 1;
|
|
40
|
+
}
|
|
41
|
+
match = CLASS_DECLARATION_PATTERN.exec(content);
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
function findMatchingBrace(content, openBraceIndex) {
|
|
46
|
+
let depth = 0;
|
|
47
|
+
for (let index = openBraceIndex; index < content.length; index += 1) {
|
|
48
|
+
const ch = content[index];
|
|
49
|
+
if (ch === '{') {
|
|
50
|
+
depth += 1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (ch === '}') {
|
|
54
|
+
depth -= 1;
|
|
55
|
+
if (depth === 0) {
|
|
56
|
+
return index;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return -1;
|
|
61
|
+
}
|
|
62
|
+
function extractHostFieldHints(classBody, serializableSymbols) {
|
|
63
|
+
const hints = new Map();
|
|
64
|
+
FIELD_DECLARATION_PATTERN.lastIndex = 0;
|
|
65
|
+
let match = FIELD_DECLARATION_PATTERN.exec(classBody);
|
|
66
|
+
while (match) {
|
|
67
|
+
const full = match[0] || '';
|
|
68
|
+
if (full.includes('(')) {
|
|
69
|
+
match = FIELD_DECLARATION_PATTERN.exec(classBody);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const declaredType = normalizeDeclaredType(match[1]);
|
|
73
|
+
const fieldName = match[2];
|
|
74
|
+
if (declaredType && serializableSymbols.has(declaredType)) {
|
|
75
|
+
hints.set(fieldName, declaredType);
|
|
76
|
+
}
|
|
77
|
+
match = FIELD_DECLARATION_PATTERN.exec(classBody);
|
|
78
|
+
}
|
|
79
|
+
return hints;
|
|
80
|
+
}
|
|
81
|
+
function normalizeDeclaredType(input) {
|
|
82
|
+
let compact = String(input || '').replace(/\s+/g, '');
|
|
83
|
+
compact = compact.replace(/^global::/, '');
|
|
84
|
+
if (!compact)
|
|
85
|
+
return null;
|
|
86
|
+
let typeName = compact;
|
|
87
|
+
while (true) {
|
|
88
|
+
const listMatch = typeName.match(/^(?:System\.Collections\.Generic\.)?(?:List|IList|IReadOnlyList|IEnumerable|HashSet)<(.+)>$/);
|
|
89
|
+
if (!listMatch)
|
|
90
|
+
break;
|
|
91
|
+
typeName = listMatch[1];
|
|
92
|
+
}
|
|
93
|
+
if (typeName.endsWith('[]')) {
|
|
94
|
+
typeName = typeName.slice(0, -2);
|
|
95
|
+
}
|
|
96
|
+
if (typeName.endsWith('?')) {
|
|
97
|
+
typeName = typeName.slice(0, -1);
|
|
98
|
+
}
|
|
99
|
+
const genericStart = typeName.indexOf('<');
|
|
100
|
+
if (genericStart !== -1) {
|
|
101
|
+
typeName = typeName.slice(0, genericStart);
|
|
102
|
+
}
|
|
103
|
+
const shortName = typeName.split('.').pop() || '';
|
|
104
|
+
return shortName || null;
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|