@veewo/gitnexus 1.4.8 → 1.4.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/dist/cli/ai-context.js +1 -2
- package/dist/cli/analyze.js +2 -1
- package/dist/cli/index.js +8 -0
- package/dist/cli/repo-manager-alias.test.js +2 -0
- package/dist/cli/tool.d.ts +11 -0
- package/dist/cli/tool.js +59 -4
- package/dist/cli/unity-ui-trace.test.d.ts +1 -0
- package/dist/cli/unity-ui-trace.test.js +24 -0
- package/dist/core/ingestion/call-processor.js +9 -1
- package/dist/core/ingestion/type-env.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/csharp.js +17 -2
- package/dist/core/ingestion/type-extractors/types.d.ts +4 -1
- package/dist/core/unity/csharp-selector-binding.d.ts +7 -0
- package/dist/core/unity/csharp-selector-binding.js +32 -0
- package/dist/core/unity/csharp-selector-binding.test.d.ts +1 -0
- package/dist/core/unity/csharp-selector-binding.test.js +14 -0
- package/dist/core/unity/scan-context.d.ts +2 -0
- package/dist/core/unity/scan-context.js +17 -0
- package/dist/core/unity/ui-asset-ref-scanner.d.ts +14 -0
- package/dist/core/unity/ui-asset-ref-scanner.js +213 -0
- package/dist/core/unity/ui-asset-ref-scanner.test.d.ts +1 -0
- package/dist/core/unity/ui-asset-ref-scanner.test.js +44 -0
- package/dist/core/unity/ui-meta-index.d.ts +8 -0
- package/dist/core/unity/ui-meta-index.js +60 -0
- package/dist/core/unity/ui-meta-index.test.d.ts +1 -0
- package/dist/core/unity/ui-meta-index.test.js +18 -0
- package/dist/core/unity/ui-trace-storage-guard.test.d.ts +1 -0
- package/dist/core/unity/ui-trace-storage-guard.test.js +10 -0
- package/dist/core/unity/ui-trace.acceptance.test.d.ts +1 -0
- package/dist/core/unity/ui-trace.acceptance.test.js +38 -0
- package/dist/core/unity/ui-trace.d.ts +31 -0
- package/dist/core/unity/ui-trace.js +363 -0
- package/dist/core/unity/ui-trace.test.d.ts +1 -0
- package/dist/core/unity/ui-trace.test.js +183 -0
- package/dist/core/unity/uss-selector-parser.d.ts +6 -0
- package/dist/core/unity/uss-selector-parser.js +21 -0
- package/dist/core/unity/uss-selector-parser.test.d.ts +1 -0
- package/dist/core/unity/uss-selector-parser.test.js +13 -0
- package/dist/core/unity/uxml-ref-parser.d.ts +10 -0
- package/dist/core/unity/uxml-ref-parser.js +22 -0
- package/dist/core/unity/uxml-ref-parser.test.d.ts +1 -0
- package/dist/core/unity/uxml-ref-parser.test.js +31 -0
- package/dist/mcp/local/local-backend.d.ts +13 -0
- package/dist/mcp/local/local-backend.js +298 -25
- package/dist/mcp/tools.js +41 -0
- package/dist/storage/repo-manager.d.ts +1 -0
- package/package.json +2 -1
- package/skills/gitnexus-cli.md +29 -0
- package/skills/gitnexus-guide.md +23 -0
package/dist/cli/ai-context.js
CHANGED
|
@@ -36,13 +36,12 @@ function generateGitNexusContent(projectName, stats, skillScope, cliPackageSpec,
|
|
|
36
36
|
return `${GITNEXUS_START_MARKER}
|
|
37
37
|
# GitNexus MCP
|
|
38
38
|
|
|
39
|
-
This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${stats.processes || 0} execution flows).
|
|
40
|
-
|
|
41
39
|
## Always Start Here
|
|
42
40
|
|
|
43
41
|
1. **Read \`gitnexus://repo/{name}/context\`** — codebase overview + check index freshness
|
|
44
42
|
2. **Match your task to a skill below** and **read that skill file**
|
|
45
43
|
3. **Follow the skill's workflow and checklist**
|
|
44
|
+
4. **Follow config/state file rules:** \`docs/gitnexus-config-files.md\`
|
|
46
45
|
|
|
47
46
|
> If step 1 warns the index is stale, ask user whether to rebuild index via \`gitnexus analyze\` when local CLI exists; otherwise resolve the pinned npx package spec from \`~/.gitnexus/config.json\` (\`cliPackageSpec\` first, then \`cliVersion\`) and run \`${reindexCmd}\` with that exact package spec (it reuses previous analyze scope/options by default; add \`--no-reuse-options\` to reset). If user declines, explicitly warn that retrieval may not reflect current codebase. For build/analyze/test commands, use a 10-30 minute timeout; on failure/timeout, report exact tool output and do not auto-retry or silently fall back to glob/grep.
|
|
48
47
|
|
package/dist/cli/analyze.js
CHANGED
|
@@ -360,9 +360,10 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
360
360
|
embeddings: embeddingCount,
|
|
361
361
|
},
|
|
362
362
|
};
|
|
363
|
+
const registeredRepo = await registerRepo(repoPath, meta, { repoAlias });
|
|
364
|
+
meta.repoId = registeredRepo.name;
|
|
363
365
|
await saveMeta(storagePath, meta);
|
|
364
366
|
await persistUnityParitySeed(storagePath, pipelineRuntime.unityResult?.paritySeed);
|
|
365
|
-
const registeredRepo = await registerRepo(repoPath, meta, { repoAlias });
|
|
366
367
|
await addToGitignore(repoPath);
|
|
367
368
|
const projectName = path.basename(repoPath);
|
|
368
369
|
let aggregatedClusterCount = 0;
|
package/dist/cli/index.js
CHANGED
|
@@ -82,6 +82,7 @@ program
|
|
|
82
82
|
.option('-g, --goal <text>', 'What you want to find')
|
|
83
83
|
.option('-l, --limit <n>', 'Max processes to return (default: 5)')
|
|
84
84
|
.option('--content', 'Include full symbol source code')
|
|
85
|
+
.option('--scope-preset <preset>', 'Scope preset for retrieval: unity-gameplay|unity-all')
|
|
85
86
|
.option('--unity-resources <mode>', 'Unity resource retrieval mode: off|on|auto', 'off')
|
|
86
87
|
.option('--unity-hydration <mode>', 'Unity hydration mode when resources are enabled: parity|compact', 'compact')
|
|
87
88
|
.action(createLazyAction(() => import('./tool.js'), 'queryCommand'));
|
|
@@ -101,6 +102,13 @@ program
|
|
|
101
102
|
.option('--target-path <path>', 'Unity project root (default: cwd)')
|
|
102
103
|
.option('--json', 'Output JSON')
|
|
103
104
|
.action(createLazyAction(() => import('./unity-bindings.js'), 'unityBindingsCommand'));
|
|
105
|
+
program
|
|
106
|
+
.command('unity-ui-trace <target>')
|
|
107
|
+
.description('Query-time Unity UI evidence tracing (asset_refs|template_refs|selector_bindings)')
|
|
108
|
+
.option('-r, --repo <name>', 'Target repository (omit if only one indexed)')
|
|
109
|
+
.option('--goal <goal>', 'Trace goal: asset_refs|template_refs|selector_bindings', 'asset_refs')
|
|
110
|
+
.option('--selector-mode <mode>', 'Selector matching mode for selector_bindings: strict|balanced', 'balanced')
|
|
111
|
+
.action(createLazyAction(() => import('./tool.js'), 'unityUiTraceCommand'));
|
|
104
112
|
program
|
|
105
113
|
.command('impact <target>')
|
|
106
114
|
.description('Blast radius analysis: what breaks if you change a symbol')
|
|
@@ -43,6 +43,7 @@ test('saveMeta/loadMeta persists analyzeOptions for future re-index reuse', asyn
|
|
|
43
43
|
const storagePath = path.join(tmpDir, '.gitnexus');
|
|
44
44
|
await saveMeta(storagePath, {
|
|
45
45
|
repoPath: tmpDir,
|
|
46
|
+
repoId: 'neonspark-v1-subset',
|
|
46
47
|
lastCommit: 'abc1234',
|
|
47
48
|
indexedAt: '2026-03-12T00:00:00.000Z',
|
|
48
49
|
analyzeOptions: {
|
|
@@ -60,4 +61,5 @@ test('saveMeta/loadMeta persists analyzeOptions for future re-index reuse', asyn
|
|
|
60
61
|
repoAlias: 'neonspark-v1-subset',
|
|
61
62
|
embeddings: true,
|
|
62
63
|
});
|
|
64
|
+
assert.equal(meta?.repoId, 'neonspark-v1-subset');
|
|
63
65
|
});
|
package/dist/cli/tool.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export declare function queryCommand(queryText: string, options?: {
|
|
|
21
21
|
goal?: string;
|
|
22
22
|
limit?: string;
|
|
23
23
|
content?: boolean;
|
|
24
|
+
scopePreset?: 'unity-gameplay' | 'unity-all';
|
|
24
25
|
unityResources?: UnityResourcesMode;
|
|
25
26
|
unityHydration?: UnityHydrationMode;
|
|
26
27
|
}): Promise<void>;
|
|
@@ -44,3 +45,13 @@ export declare function impactCommand(target: string, options?: {
|
|
|
44
45
|
export declare function cypherCommand(query: string, options?: {
|
|
45
46
|
repo?: string;
|
|
46
47
|
}): Promise<void>;
|
|
48
|
+
export declare function unityUiTraceCommand(target: string, options?: {
|
|
49
|
+
repo?: string;
|
|
50
|
+
goal?: string;
|
|
51
|
+
selectorMode?: string;
|
|
52
|
+
}, deps?: {
|
|
53
|
+
backend?: {
|
|
54
|
+
callTool: (method: string, params: any) => Promise<any>;
|
|
55
|
+
};
|
|
56
|
+
output?: (data: any) => void;
|
|
57
|
+
}): Promise<void>;
|
package/dist/cli/tool.js
CHANGED
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
* See the output() function for details (#324).
|
|
16
16
|
*/
|
|
17
17
|
import { writeSync } from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
18
19
|
import { LocalBackend } from '../mcp/local/local-backend.js';
|
|
20
|
+
import { getGitRoot } from '../storage/git.js';
|
|
21
|
+
import { getStoragePaths, listRegisteredRepos, loadMeta } from '../storage/repo-manager.js';
|
|
19
22
|
let _backend = null;
|
|
20
23
|
async function getBackend() {
|
|
21
24
|
if (_backend)
|
|
@@ -53,21 +56,45 @@ function output(data) {
|
|
|
53
56
|
process.stderr.write(text + '\n');
|
|
54
57
|
}
|
|
55
58
|
}
|
|
59
|
+
function isUnityUiTraceGoal(value) {
|
|
60
|
+
return value === 'asset_refs' || value === 'template_refs' || value === 'selector_bindings';
|
|
61
|
+
}
|
|
62
|
+
function isUnityUiSelectorMode(value) {
|
|
63
|
+
return value === 'strict' || value === 'balanced';
|
|
64
|
+
}
|
|
65
|
+
async function resolveRepoOption(explicitRepo) {
|
|
66
|
+
if (explicitRepo?.trim())
|
|
67
|
+
return explicitRepo.trim();
|
|
68
|
+
const gitRoot = getGitRoot(process.cwd());
|
|
69
|
+
if (!gitRoot)
|
|
70
|
+
return undefined;
|
|
71
|
+
const { storagePath } = getStoragePaths(gitRoot);
|
|
72
|
+
const meta = await loadMeta(storagePath);
|
|
73
|
+
const repoId = typeof meta?.repoId === 'string' ? meta.repoId.trim() : '';
|
|
74
|
+
if (repoId)
|
|
75
|
+
return repoId;
|
|
76
|
+
// Backward compatibility for indexes created before repoId persisted in meta.json.
|
|
77
|
+
const entries = await listRegisteredRepos({ validate: false });
|
|
78
|
+
const matched = entries.find((entry) => path.resolve(entry.path) === gitRoot);
|
|
79
|
+
return matched?.name || undefined;
|
|
80
|
+
}
|
|
56
81
|
export async function queryCommand(queryText, options) {
|
|
57
82
|
if (!queryText?.trim()) {
|
|
58
83
|
console.error('Usage: gitnexus query <search_query>');
|
|
59
84
|
process.exit(1);
|
|
60
85
|
}
|
|
61
86
|
const backend = await getBackend();
|
|
87
|
+
const repo = await resolveRepoOption(options?.repo);
|
|
62
88
|
const result = await backend.callTool('query', {
|
|
63
89
|
query: queryText,
|
|
64
90
|
task_context: options?.context,
|
|
65
91
|
goal: options?.goal,
|
|
66
92
|
limit: options?.limit ? parseInt(options.limit) : undefined,
|
|
67
93
|
include_content: options?.content ?? false,
|
|
94
|
+
scope_preset: options?.scopePreset,
|
|
68
95
|
unity_resources: options?.unityResources,
|
|
69
96
|
unity_hydration_mode: options?.unityHydration,
|
|
70
|
-
repo
|
|
97
|
+
repo,
|
|
71
98
|
});
|
|
72
99
|
output(result);
|
|
73
100
|
}
|
|
@@ -77,6 +104,7 @@ export async function contextCommand(name, options) {
|
|
|
77
104
|
process.exit(1);
|
|
78
105
|
}
|
|
79
106
|
const backend = await getBackend();
|
|
107
|
+
const repo = await resolveRepoOption(options?.repo);
|
|
80
108
|
const result = await backend.callTool('context', {
|
|
81
109
|
name: name || undefined,
|
|
82
110
|
uid: options?.uid,
|
|
@@ -84,7 +112,7 @@ export async function contextCommand(name, options) {
|
|
|
84
112
|
include_content: options?.content ?? false,
|
|
85
113
|
unity_resources: options?.unityResources,
|
|
86
114
|
unity_hydration_mode: options?.unityHydration,
|
|
87
|
-
repo
|
|
115
|
+
repo,
|
|
88
116
|
});
|
|
89
117
|
output(result);
|
|
90
118
|
}
|
|
@@ -95,6 +123,7 @@ export async function impactCommand(target, options) {
|
|
|
95
123
|
}
|
|
96
124
|
try {
|
|
97
125
|
const backend = await getBackend();
|
|
126
|
+
const repo = await resolveRepoOption(options?.repo);
|
|
98
127
|
const result = await backend.callTool('impact', {
|
|
99
128
|
target,
|
|
100
129
|
target_uid: options?.uid,
|
|
@@ -103,7 +132,7 @@ export async function impactCommand(target, options) {
|
|
|
103
132
|
maxDepth: options?.depth ? parseInt(options.depth, 10) : undefined,
|
|
104
133
|
minConfidence: options?.minConfidence ? parseFloat(options.minConfidence) : undefined,
|
|
105
134
|
includeTests: options?.includeTests ?? false,
|
|
106
|
-
repo
|
|
135
|
+
repo,
|
|
107
136
|
});
|
|
108
137
|
output(result);
|
|
109
138
|
}
|
|
@@ -124,9 +153,35 @@ export async function cypherCommand(query, options) {
|
|
|
124
153
|
process.exit(1);
|
|
125
154
|
}
|
|
126
155
|
const backend = await getBackend();
|
|
156
|
+
const repo = await resolveRepoOption(options?.repo);
|
|
127
157
|
const result = await backend.callTool('cypher', {
|
|
128
158
|
query,
|
|
129
|
-
repo
|
|
159
|
+
repo,
|
|
130
160
|
});
|
|
131
161
|
output(result);
|
|
132
162
|
}
|
|
163
|
+
export async function unityUiTraceCommand(target, options, deps) {
|
|
164
|
+
if (!target?.trim()) {
|
|
165
|
+
console.error('Usage: gitnexus unity-ui-trace <target> --goal asset_refs|template_refs|selector_bindings');
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
const goal = String(options?.goal || 'asset_refs').trim();
|
|
169
|
+
if (!isUnityUiTraceGoal(goal)) {
|
|
170
|
+
console.error('Invalid --goal. Use one of: asset_refs, template_refs, selector_bindings');
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
const selectorMode = String(options?.selectorMode || 'balanced').trim();
|
|
174
|
+
if (!isUnityUiSelectorMode(selectorMode)) {
|
|
175
|
+
console.error('Invalid --selector-mode. Use one of: strict, balanced');
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
const backend = deps?.backend || await getBackend();
|
|
179
|
+
const repo = await resolveRepoOption(options?.repo);
|
|
180
|
+
const result = await backend.callTool('unity_ui_trace', {
|
|
181
|
+
target,
|
|
182
|
+
goal,
|
|
183
|
+
selector_mode: selectorMode,
|
|
184
|
+
repo,
|
|
185
|
+
});
|
|
186
|
+
(deps?.output || output)(result);
|
|
187
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { unityUiTraceCommand } from './tool.js';
|
|
4
|
+
test('unity-ui-trace command forwards params and prints result', async () => {
|
|
5
|
+
const calls = [];
|
|
6
|
+
let printed = null;
|
|
7
|
+
await unityUiTraceCommand('EliteBossScreenController', { goal: 'selector_bindings', selectorMode: 'strict', repo: 'mini-unity-ui' }, {
|
|
8
|
+
backend: {
|
|
9
|
+
async callTool(method, params) {
|
|
10
|
+
calls.push({ method, params });
|
|
11
|
+
return { goal: 'selector_bindings', results: [{ evidence_chain: [{ path: 'a', line: 1 }] }], diagnostics: [] };
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
output: (value) => {
|
|
15
|
+
printed = value;
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
assert.equal(calls.length, 1);
|
|
19
|
+
assert.equal(calls[0].method, 'unity_ui_trace');
|
|
20
|
+
assert.equal(calls[0].params.target, 'EliteBossScreenController');
|
|
21
|
+
assert.equal(calls[0].params.goal, 'selector_bindings');
|
|
22
|
+
assert.equal(calls[0].params.selector_mode, 'strict');
|
|
23
|
+
assert.equal(printed.goal, 'selector_bindings');
|
|
24
|
+
});
|
|
@@ -35,7 +35,15 @@ const findEnclosingFunction = (node, filePath, ctx) => {
|
|
|
35
35
|
*/
|
|
36
36
|
const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
|
|
37
37
|
const verified = new Map();
|
|
38
|
-
for (const { scope, varName, calleeName, receiverClassName } of bindings) {
|
|
38
|
+
for (const { scope, varName, calleeName, receiverClassName, inferredTypeName } of bindings) {
|
|
39
|
+
if (inferredTypeName) {
|
|
40
|
+
const inferred = ctx.resolve(inferredTypeName, filePath);
|
|
41
|
+
const isReceivableType = inferred?.candidates.some(def => def.type === 'Class' || def.type === 'Interface' || def.type === 'Struct' || def.type === 'Enum') ?? false;
|
|
42
|
+
if (isReceivableType) {
|
|
43
|
+
verified.set(receiverKey(scope, varName), inferredTypeName);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
39
47
|
const tiered = ctx.resolve(calleeName, filePath);
|
|
40
48
|
const isClass = tiered?.candidates.some(def => def.type === 'Class') ?? false;
|
|
41
49
|
if (isClass) {
|
|
@@ -46,4 +46,6 @@ export interface ConstructorBinding {
|
|
|
46
46
|
calleeName: string;
|
|
47
47
|
/** Enclosing class name when callee is a method on a known receiver (e.g. $this) */
|
|
48
48
|
receiverClassName?: string;
|
|
49
|
+
/** Optional direct type hint extracted from safe generic invocation patterns. */
|
|
50
|
+
inferredTypeName?: string;
|
|
49
51
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { extractSimpleTypeName, extractVarName, findChildByType, unwrapAwait, resolveIterableElementType, methodToTypeArgPosition, extractElementTypeFromString } from './shared.js';
|
|
1
|
+
import { extractSimpleTypeName, extractVarName, findChildByType, unwrapAwait, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, extractElementTypeFromString } from './shared.js';
|
|
2
2
|
/** Known container property accessors that operate on the container itself (e.g., dict.Keys, dict.Values) */
|
|
3
3
|
const KNOWN_CONTAINER_PROPS = new Set(['Keys', 'Values']);
|
|
4
4
|
const DECLARATION_NODE_TYPES = new Set([
|
|
@@ -145,7 +145,22 @@ const scanConstructorBinding = (node) => {
|
|
|
145
145
|
const calleeName = extractSimpleTypeName(func);
|
|
146
146
|
if (!calleeName)
|
|
147
147
|
return undefined;
|
|
148
|
-
|
|
148
|
+
// Safe generic-invocation receiver hint:
|
|
149
|
+
// var user = GetComponentInParent<User>()
|
|
150
|
+
// var user = svc.GetComponentInParent<User>()
|
|
151
|
+
// infer user -> User directly from the single type argument.
|
|
152
|
+
let inferredTypeName;
|
|
153
|
+
const genericCallee = func.type === 'generic_name'
|
|
154
|
+
? func
|
|
155
|
+
: func.type === 'member_access_expression'
|
|
156
|
+
? func.childForFieldName('name')
|
|
157
|
+
: null;
|
|
158
|
+
if (genericCallee?.type === 'generic_name') {
|
|
159
|
+
const typeArgs = extractGenericTypeArgs(genericCallee);
|
|
160
|
+
if (typeArgs.length === 1)
|
|
161
|
+
inferredTypeName = typeArgs[0];
|
|
162
|
+
}
|
|
163
|
+
return { varName: nameNode.text, calleeName, inferredTypeName };
|
|
149
164
|
};
|
|
150
165
|
const FOR_LOOP_NODE_TYPES = new Set([
|
|
151
166
|
'foreach_statement',
|
|
@@ -13,11 +13,14 @@ export type InitializerExtractor = (node: SyntaxNode, env: Map<string, string>,
|
|
|
13
13
|
/** Scans an AST node for untyped `var = callee()` patterns for return-type inference.
|
|
14
14
|
* Returns { varName, calleeName } if the node matches, undefined otherwise.
|
|
15
15
|
* `receiverClassName` — optional hint for method calls on known receivers
|
|
16
|
-
* (e.g. $this->getUser() in PHP provides the enclosing class name).
|
|
16
|
+
* (e.g. $this->getUser() in PHP provides the enclosing class name).
|
|
17
|
+
* `inferredTypeName` — optional direct type hint from safe generic invocations
|
|
18
|
+
* (e.g. `var x = GetComponentInParent<User>()` gives `User`). */
|
|
17
19
|
export type ConstructorBindingScanner = (node: SyntaxNode) => {
|
|
18
20
|
varName: string;
|
|
19
21
|
calleeName: string;
|
|
20
22
|
receiverClassName?: string;
|
|
23
|
+
inferredTypeName?: string;
|
|
21
24
|
} | undefined;
|
|
22
25
|
/** Extracts a return type string from a method/function definition node.
|
|
23
26
|
* Used for languages where return types are expressed in comments (e.g. YARD @return [Type])
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const ADD_TO_CLASS_LIST_LITERAL = /\bAddToClassList\s*\(\s*"([^"]+)"\s*\)/g;
|
|
2
|
+
const Q_CLASS_NAME_LITERAL = /\bQ\s*<[^>]*>\s*\([^)]*\bclassName\s*:\s*"([^"]+)"[^)]*\)/g;
|
|
3
|
+
export function extractCsharpSelectorBindings(content) {
|
|
4
|
+
const out = [];
|
|
5
|
+
const lines = content.split(/\r?\n/);
|
|
6
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
7
|
+
const line = lines[i];
|
|
8
|
+
ADD_TO_CLASS_LIST_LITERAL.lastIndex = 0;
|
|
9
|
+
let addMatch = ADD_TO_CLASS_LIST_LITERAL.exec(line);
|
|
10
|
+
while (addMatch) {
|
|
11
|
+
out.push({
|
|
12
|
+
className: addMatch[1],
|
|
13
|
+
line: i + 1,
|
|
14
|
+
snippet: line.trim(),
|
|
15
|
+
isDynamic: false,
|
|
16
|
+
});
|
|
17
|
+
addMatch = ADD_TO_CLASS_LIST_LITERAL.exec(line);
|
|
18
|
+
}
|
|
19
|
+
Q_CLASS_NAME_LITERAL.lastIndex = 0;
|
|
20
|
+
let qMatch = Q_CLASS_NAME_LITERAL.exec(line);
|
|
21
|
+
while (qMatch) {
|
|
22
|
+
out.push({
|
|
23
|
+
className: qMatch[1],
|
|
24
|
+
line: i + 1,
|
|
25
|
+
snippet: line.trim(),
|
|
26
|
+
isDynamic: false,
|
|
27
|
+
});
|
|
28
|
+
qMatch = Q_CLASS_NAME_LITERAL.exec(line);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { extractCsharpSelectorBindings } from './csharp-selector-binding.js';
|
|
7
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const fixtureRoot = path.resolve(here, '../../../src/core/unity/__fixtures__/mini-unity-ui');
|
|
9
|
+
test('extracts static-only selector bindings from csharp', async () => {
|
|
10
|
+
const source = await fs.readFile(path.join(fixtureRoot, 'Assets/Scripts/EliteBossScreenController.cs'), 'utf-8');
|
|
11
|
+
const bindings = extractCsharpSelectorBindings(source);
|
|
12
|
+
assert.equal(bindings.some((entry) => entry.className === 'tooltip-box'), true);
|
|
13
|
+
assert.equal(bindings.some((entry) => entry.isDynamic), false);
|
|
14
|
+
});
|
|
@@ -19,6 +19,8 @@ export interface UnityScanContext {
|
|
|
19
19
|
serializableSymbols: Set<string>;
|
|
20
20
|
hostFieldTypeHints: Map<string, Map<string, string>>;
|
|
21
21
|
assetGuidToPath?: Map<string, string>;
|
|
22
|
+
uxmlGuidToPath?: Map<string, string>;
|
|
23
|
+
ussGuidToPath?: Map<string, string>;
|
|
22
24
|
resourceDocCache: Map<string, UnityObjectBlock[]>;
|
|
23
25
|
}
|
|
24
26
|
export declare function buildUnityScanContext(input: BuildScanContextInput): Promise<UnityScanContext>;
|
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { createInterface } from 'node:readline';
|
|
5
5
|
import { glob } from 'glob';
|
|
6
6
|
import { buildAssetMetaIndex, buildMetaIndex } from './meta-index.js';
|
|
7
|
+
import { buildUnityUiMetaIndex } from './ui-meta-index.js';
|
|
7
8
|
import { buildSerializableTypeIndexFromFiles } from './serialized-type-index.js';
|
|
8
9
|
const DECLARATION_PATTERN = /\b(?:class|struct|interface)\s+([A-Za-z_][A-Za-z0-9_]*)\b/g;
|
|
9
10
|
const SCRIPT_GUID_IN_LINE_PATTERN = /\bm_Script\s*:\s*\{[^}]*\bguid\s*:\s*([0-9a-f]{32})\b/gi;
|
|
@@ -32,6 +33,7 @@ export async function buildUnityScanContext(input) {
|
|
|
32
33
|
const guidToResourceHits = await buildGuidHitIndex(input.repoRoot, scriptPathToGuid, resourceFiles);
|
|
33
34
|
const assetMetaFiles = resolveAssetMetaFiles(input.repoRoot, input.scopedPaths, scriptFiles, resourceFiles);
|
|
34
35
|
const assetGuidToPath = await buildAssetMetaIndex(input.repoRoot, { metaFiles: assetMetaFiles });
|
|
36
|
+
const uiMetaIndex = await buildUnityUiMetaIndex(input.repoRoot, { scopedPaths: input.scopedPaths });
|
|
35
37
|
const symbolToCanonicalScriptPath = buildCanonicalScriptPathIndex(symbolToScriptPaths, scriptPathToGuid, guidToResourceHits);
|
|
36
38
|
const symbolToScriptPath = new Map(symbolToCanonicalScriptPath);
|
|
37
39
|
return {
|
|
@@ -43,6 +45,8 @@ export async function buildUnityScanContext(input) {
|
|
|
43
45
|
serializableSymbols: serializableTypeIndex.serializableSymbols,
|
|
44
46
|
hostFieldTypeHints: serializableTypeIndex.hostFieldTypeHints,
|
|
45
47
|
assetGuidToPath,
|
|
48
|
+
uxmlGuidToPath: uiMetaIndex.uxmlGuidToPath,
|
|
49
|
+
ussGuidToPath: uiMetaIndex.ussGuidToPath,
|
|
46
50
|
resourceDocCache: new Map(),
|
|
47
51
|
};
|
|
48
52
|
}
|
|
@@ -114,11 +118,22 @@ export function buildUnityScanContextFromSeed(input) {
|
|
|
114
118
|
}
|
|
115
119
|
}
|
|
116
120
|
const assetGuidToPath = new Map();
|
|
121
|
+
const uxmlGuidToPath = new Map();
|
|
122
|
+
const ussGuidToPath = new Map();
|
|
117
123
|
for (const [guid, assetPath] of Object.entries(seed.assetGuidToPath || {})) {
|
|
118
124
|
const normalizedPath = normalizeSlashes(String(assetPath || '').trim());
|
|
119
125
|
if (!guid || !normalizedPath)
|
|
120
126
|
continue;
|
|
121
127
|
assetGuidToPath.set(guid, normalizedPath);
|
|
128
|
+
assetGuidToPath.set(guid.toLowerCase(), normalizedPath);
|
|
129
|
+
if (normalizedPath.endsWith('.uxml')) {
|
|
130
|
+
uxmlGuidToPath.set(guid, normalizedPath);
|
|
131
|
+
uxmlGuidToPath.set(guid.toLowerCase(), normalizedPath);
|
|
132
|
+
}
|
|
133
|
+
if (normalizedPath.endsWith('.uss')) {
|
|
134
|
+
ussGuidToPath.set(guid, normalizedPath);
|
|
135
|
+
ussGuidToPath.set(guid.toLowerCase(), normalizedPath);
|
|
136
|
+
}
|
|
122
137
|
}
|
|
123
138
|
return {
|
|
124
139
|
symbolToScriptPaths,
|
|
@@ -129,6 +144,8 @@ export function buildUnityScanContextFromSeed(input) {
|
|
|
129
144
|
serializableSymbols: new Set(),
|
|
130
145
|
hostFieldTypeHints: new Map(),
|
|
131
146
|
assetGuidToPath,
|
|
147
|
+
uxmlGuidToPath,
|
|
148
|
+
ussGuidToPath,
|
|
132
149
|
resourceDocCache: new Map(),
|
|
133
150
|
};
|
|
134
151
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface UiAssetRefEvidence {
|
|
2
|
+
sourceType: 'prefab' | 'asset';
|
|
3
|
+
sourcePath: string;
|
|
4
|
+
line: number;
|
|
5
|
+
fieldName: string;
|
|
6
|
+
guid: string;
|
|
7
|
+
snippet: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ScanUiAssetRefInput {
|
|
10
|
+
repoRoot: string;
|
|
11
|
+
scopedPaths?: string[];
|
|
12
|
+
targetGuids?: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function scanUiAssetRefs(input: ScanUiAssetRefInput): Promise<UiAssetRefEvidence[]>;
|