gitnexus 1.6.11-rc.2 → 1.6.11-rc.21
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 +43 -1
- package/dist/_shared/impact-risk.d.ts +37 -0
- package/dist/_shared/impact-risk.d.ts.map +1 -0
- package/dist/_shared/impact-risk.js +92 -0
- package/dist/_shared/impact-risk.js.map +1 -0
- package/dist/_shared/index.d.ts +2 -0
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +2 -0
- package/dist/_shared/index.js.map +1 -1
- package/dist/cli/ai-context.js +4 -4
- package/dist/cli/analyze-config.d.ts +2 -0
- package/dist/cli/analyze-config.js +16 -0
- package/dist/cli/analyze-options.d.ts +4 -0
- package/dist/cli/analyze.d.ts +17 -0
- package/dist/cli/analyze.js +43 -7
- package/dist/cli/eval-server.js +22 -5
- package/dist/cli/group.js +7 -1
- package/dist/cli/help-i18n.js +2 -0
- package/dist/cli/i18n/en.d.ts +12 -2
- package/dist/cli/i18n/en.js +12 -2
- package/dist/cli/i18n/resources.d.ts +22 -2
- package/dist/cli/i18n/zh-CN.d.ts +10 -0
- package/dist/cli/i18n/zh-CN.js +12 -2
- package/dist/cli/index.js +11 -4
- package/dist/cli/status.js +91 -6
- package/dist/cli/watch-queue.d.ts +41 -0
- package/dist/cli/watch-queue.js +184 -0
- package/dist/cli/watch.d.ts +20 -0
- package/dist/cli/watch.js +372 -0
- package/dist/cli/wiki.js +15 -2
- package/dist/config/ignore-service.d.ts +11 -0
- package/dist/config/ignore-service.js +41 -4
- package/dist/config/repo-control-file.d.ts +3 -0
- package/dist/config/repo-control-file.js +115 -0
- package/dist/core/group/config-parser.js +20 -2
- package/dist/core/group/cross-impact.d.ts +2 -1
- package/dist/core/group/cross-impact.js +33 -2
- package/dist/core/group/extractors/fs-utils.d.ts +2 -0
- package/dist/core/group/extractors/fs-utils.js +86 -0
- package/dist/core/group/extractors/graphql-extractor.d.ts +17 -0
- package/dist/core/group/extractors/graphql-extractor.js +652 -0
- package/dist/core/group/extractors/java-workspace-extractor.js +244 -31
- package/dist/core/group/extractors/manifest-extractor.d.ts +1 -1
- package/dist/core/group/extractors/manifest-extractor.js +1 -1
- package/dist/core/group/matching.js +8 -1
- package/dist/core/group/service.js +5 -1
- package/dist/core/group/storage.js +1 -0
- package/dist/core/group/sync.d.ts +3 -1
- package/dist/core/group/sync.js +43 -5
- package/dist/core/group/types.d.ts +19 -5
- package/dist/core/incremental/derived-writeback.d.ts +36 -0
- package/dist/core/incremental/derived-writeback.js +68 -0
- package/dist/core/incremental/subgraph-extract.d.ts +6 -4
- package/dist/core/incremental/subgraph-extract.js +7 -5
- package/dist/core/index-content-drift.d.ts +54 -0
- package/dist/core/index-content-drift.js +127 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +15 -5
- package/dist/core/ingestion/filesystem-walker.js +20 -3
- package/dist/core/ingestion/frameworks/spring/dynamic-lookups.d.ts +22 -0
- package/dist/core/ingestion/frameworks/spring/dynamic-lookups.js +124 -0
- package/dist/core/ingestion/language-provider.d.ts +43 -0
- package/dist/core/ingestion/languages/csharp/razor-view-components.d.ts +62 -0
- package/dist/core/ingestion/languages/csharp/razor-view-components.js +954 -0
- package/dist/core/ingestion/languages/csharp/resolution-config.d.ts +3 -0
- package/dist/core/ingestion/languages/csharp/resolution-config.js +6 -1
- package/dist/core/ingestion/languages/csharp/scope-resolver.js +8 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +4 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.js +16 -0
- package/dist/core/ingestion/languages/java/captures.js +14 -1
- package/dist/core/ingestion/languages/java/lombok-synthesizer.d.ts +42 -0
- package/dist/core/ingestion/languages/java/lombok-synthesizer.js +439 -0
- package/dist/core/ingestion/languages/java/scope-resolver.js +2 -0
- package/dist/core/ingestion/languages/java/spring-dynamic-lookup.d.ts +8 -0
- package/dist/core/ingestion/languages/java/spring-dynamic-lookup.js +62 -0
- package/dist/core/ingestion/languages/java.js +2 -0
- package/dist/core/ingestion/languages/jvm/accessor-synthesis.d.ts +99 -0
- package/dist/core/ingestion/languages/jvm/accessor-synthesis.js +173 -0
- package/dist/core/ingestion/languages/jvm/beanspec.d.ts +17 -0
- package/dist/core/ingestion/languages/jvm/beanspec.js +42 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +5 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +16 -0
- package/dist/core/ingestion/languages/kotlin/captures.js +14 -1
- package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.d.ts +26 -0
- package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.js +417 -0
- package/dist/core/ingestion/languages/kotlin/scope-resolver.js +2 -0
- package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.d.ts +8 -0
- package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.js +77 -0
- package/dist/core/ingestion/languages/kotlin.js +2 -0
- package/dist/core/ingestion/pipeline-phases/di.js +47 -14
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +3 -1
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +57 -86
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +2 -0
- package/dist/core/ingestion/pipeline-phases/runner.d.ts +4 -1
- package/dist/core/ingestion/pipeline-phases/runner.js +34 -14
- package/dist/core/ingestion/pipeline-phases/scan.js +25 -13
- package/dist/core/ingestion/pipeline.d.ts +6 -0
- package/dist/core/ingestion/pipeline.js +44 -12
- package/dist/core/ingestion/scope-extractor.js +1 -0
- package/dist/core/ingestion/utils/symbol-labels.d.ts +2 -2
- package/dist/core/ingestion/utils/symbol-labels.js +2 -2
- package/dist/core/ingestion/workers/parse-worker.js +28 -2
- package/dist/core/lbug/lbug-adapter.d.ts +59 -0
- package/dist/core/lbug/lbug-adapter.js +154 -1
- package/dist/core/run-analyze.d.ts +17 -0
- package/dist/core/run-analyze.js +231 -23
- package/dist/core/search/fts-indexes.d.ts +19 -1
- package/dist/core/search/fts-indexes.js +28 -1
- package/dist/core/wiki/generator.js +8 -0
- package/dist/core/wiki/grok-client.d.ts +21 -0
- package/dist/core/wiki/grok-client.js +287 -0
- package/dist/core/wiki/llm-client.d.ts +1 -1
- package/dist/core/wiki/llm-client.js +5 -2
- package/dist/core/wiki/local-cli-client.d.ts +11 -0
- package/dist/core/wiki/local-cli-client.js +22 -9
- package/dist/mcp/local/local-backend.d.ts +24 -7
- package/dist/mcp/local/local-backend.js +190 -81
- package/dist/mcp/local/pdg-impact.d.ts +8 -4
- package/dist/mcp/local/pdg-impact.js +7 -2
- package/dist/mcp/repository-policy.d.ts +5 -1
- package/dist/mcp/repository-policy.js +48 -4
- package/dist/mcp/resources.js +2 -1
- package/dist/mcp/server.js +6 -5
- package/dist/mcp/tools.js +31 -19
- package/dist/server/api.js +23 -64
- package/dist/server/grep-params.d.ts +18 -0
- package/dist/server/grep-params.js +83 -0
- package/dist/server/grep-scan.d.ts +23 -0
- package/dist/server/grep-scan.js +107 -0
- package/dist/server/grep-worker.d.ts +1 -0
- package/dist/server/grep-worker.js +12 -0
- package/dist/server/mcp-http.d.ts +8 -0
- package/dist/server/mcp-http.js +16 -1
- package/dist/storage/file-hash.d.ts +5 -0
- package/dist/storage/file-hash.js +16 -6
- package/dist/storage/fs-atomic.d.ts +24 -0
- package/dist/storage/fs-atomic.js +86 -2
- package/dist/storage/git.d.ts +19 -8
- package/dist/storage/git.js +83 -24
- package/dist/storage/gitnexus-managed-paths.d.ts +36 -0
- package/dist/storage/gitnexus-managed-paths.js +46 -0
- package/dist/storage/parse-cache.d.ts +22 -4
- package/dist/storage/parse-cache.js +106 -29
- package/dist/storage/parsedfile-store.d.ts +34 -60
- package/dist/storage/parsedfile-store.js +177 -171
- package/dist/storage/repo-manager.d.ts +11 -1
- package/dist/storage/repo-manager.js +23 -2
- package/dist/storage/repo-meta.d.ts +12 -0
- package/dist/storage/v8-sidecar.d.ts +48 -0
- package/dist/storage/v8-sidecar.js +347 -0
- package/dist/types/pipeline.d.ts +14 -0
- package/package.json +4 -1
- package/scripts/cross-platform-shard.ts +4 -2
- package/scripts/cross-platform-tests.ts +7 -1
- package/skills/gitnexus-cli.md +11 -3
- package/skills/gitnexus-impact-analysis.md +9 -0
- package/web/assets/{agent-Dr4l5EOp.js → agent-CFqT4hjR.js} +108 -104
- package/web/assets/{index-2zdvEdzg.js → index-BMIniRtX.js} +3 -3
- package/web/index.html +1 -1
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
import { glob } from 'glob';
|
|
2
|
+
import { Kind, parse, } from 'graphql';
|
|
3
|
+
import Parser from 'tree-sitter';
|
|
4
|
+
import TypeScript from 'tree-sitter-typescript';
|
|
5
|
+
import { createIgnoreFilter } from '../../../config/ignore-service.js';
|
|
6
|
+
import { getMaxFileSizeBytes } from '../../ingestion/utils/max-file-size.js';
|
|
7
|
+
import { logger } from '../../logger.js';
|
|
8
|
+
import { ParseTimeoutError, parseSourceSafe } from '../../tree-sitter/safe-parse.js';
|
|
9
|
+
import { readSafeBounded } from './fs-utils.js';
|
|
10
|
+
const PROVIDER_GLOB = '**/*.{ts,tsx,mts,cts}';
|
|
11
|
+
const DOCUMENT_GLOB = '**/*.{graphql,gql}';
|
|
12
|
+
const NEST_GRAPHQL_PACKAGE = '@nestjs/graphql';
|
|
13
|
+
const MAX_GRAPHQL_TOKENS = 100_000;
|
|
14
|
+
const MAX_GRAPHQL_DEFINITIONS = 5_000;
|
|
15
|
+
const MAX_GRAPHQL_OPERATIONS = 500;
|
|
16
|
+
const MAX_GRAPHQL_SELECTIONS = 10_000;
|
|
17
|
+
const MAX_GRAPHQL_TRAVERSAL_DEPTH = 64;
|
|
18
|
+
const MAX_PROVIDER_AST_NODES = 100_000;
|
|
19
|
+
const MAX_PROVIDER_AST_DEPTH = 256;
|
|
20
|
+
const GRAPHQL_NAME = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
|
21
|
+
export const RESOLVE_METHOD_QUERY = `
|
|
22
|
+
MATCH (n)
|
|
23
|
+
WHERE labels(n) IN ['Method','Function','Property','CodeElement']
|
|
24
|
+
AND n.name = $name AND n.filePath = $filePath AND n.startLine = $startLine AND n.id <> ''
|
|
25
|
+
RETURN n.id AS uid, n.name AS name, n.filePath AS filePath
|
|
26
|
+
ORDER BY n.id ASC
|
|
27
|
+
LIMIT 2`;
|
|
28
|
+
// LadybugDB returns labels(n) as a scalar string, not Neo4j's string array.
|
|
29
|
+
// The real-db integration test executes this exact query and guards that dialect contract.
|
|
30
|
+
export const RESOLVE_GENERATED_SYMBOL_QUERY = `
|
|
31
|
+
MATCH (n)
|
|
32
|
+
WHERE labels(n) IN ['Const','Variable','Function','Method','CodeElement']
|
|
33
|
+
AND n.name = $name AND n.filePath <> '' AND n.id <> ''
|
|
34
|
+
RETURN n.id AS uid, n.name AS name, n.filePath AS filePath
|
|
35
|
+
ORDER BY n.id ASC
|
|
36
|
+
LIMIT 2`;
|
|
37
|
+
function rowValue(row, key, position) {
|
|
38
|
+
return String(row[key] ?? row[position] ?? '');
|
|
39
|
+
}
|
|
40
|
+
function uniqueRealSymbol(rows) {
|
|
41
|
+
if (rows.length !== 1)
|
|
42
|
+
return null;
|
|
43
|
+
const row = rows[0];
|
|
44
|
+
const symbol = {
|
|
45
|
+
uid: rowValue(row, 'uid', 0),
|
|
46
|
+
name: rowValue(row, 'name', 1),
|
|
47
|
+
filePath: rowValue(row, 'filePath', 2).replace(/\\/g, '/'),
|
|
48
|
+
};
|
|
49
|
+
return symbol.uid && symbol.name && symbol.filePath ? symbol : null;
|
|
50
|
+
}
|
|
51
|
+
function unquote(text) {
|
|
52
|
+
const trimmed = text.trim();
|
|
53
|
+
if (trimmed.length < 2)
|
|
54
|
+
return null;
|
|
55
|
+
const quote = trimmed[0];
|
|
56
|
+
if ((quote !== "'" && quote !== '"' && quote !== '`') || trimmed.at(-1) !== quote)
|
|
57
|
+
return null;
|
|
58
|
+
const value = trimmed.slice(1, -1);
|
|
59
|
+
return value.includes('${') ? null : value;
|
|
60
|
+
}
|
|
61
|
+
function unwrapExpression(node) {
|
|
62
|
+
let current = node;
|
|
63
|
+
while (['as_expression', 'satisfies_expression', 'parenthesized_expression'].includes(current.type) &&
|
|
64
|
+
current.namedChildren[0]) {
|
|
65
|
+
current = current.namedChildren[0];
|
|
66
|
+
}
|
|
67
|
+
return current;
|
|
68
|
+
}
|
|
69
|
+
function objectPairValue(node, key) {
|
|
70
|
+
const object = unwrapExpression(node);
|
|
71
|
+
if (object.type !== 'object')
|
|
72
|
+
return null;
|
|
73
|
+
for (const pair of object.namedChildren) {
|
|
74
|
+
if (pair.type !== 'pair')
|
|
75
|
+
continue;
|
|
76
|
+
const keyNode = pair.childForFieldName('key');
|
|
77
|
+
const pairKey = keyNode ? (unquote(keyNode.text) ?? keyNode.text) : null;
|
|
78
|
+
if (pairKey === key)
|
|
79
|
+
return pair.childForFieldName('value');
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
function literalValue(node) {
|
|
84
|
+
return node ? unquote(unwrapExpression(node).text) : null;
|
|
85
|
+
}
|
|
86
|
+
function graphqlNameValue(node) {
|
|
87
|
+
return node ? literalValue(objectPairValue(node, 'value')) : null;
|
|
88
|
+
}
|
|
89
|
+
function withinGeneratedAstBudget(root) {
|
|
90
|
+
const pending = [{ node: root, depth: 0 }];
|
|
91
|
+
let visited = 0;
|
|
92
|
+
while (pending.length > 0) {
|
|
93
|
+
const current = pending.pop();
|
|
94
|
+
if (!current)
|
|
95
|
+
break;
|
|
96
|
+
visited++;
|
|
97
|
+
if (visited > MAX_PROVIDER_AST_NODES || current.depth > MAX_PROVIDER_AST_DEPTH)
|
|
98
|
+
return false;
|
|
99
|
+
for (let index = current.node.namedChildren.length - 1; index >= 0; index--) {
|
|
100
|
+
const child = current.node.namedChildren[index];
|
|
101
|
+
if (child)
|
|
102
|
+
pending.push({ node: child, depth: current.depth + 1 });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
function generatedRootFields(selectionSet, fragments) {
|
|
108
|
+
const fields = new Set();
|
|
109
|
+
if (!selectionSet)
|
|
110
|
+
return null;
|
|
111
|
+
const seenFragments = new Set();
|
|
112
|
+
const pending = [
|
|
113
|
+
{ selectionSet, depth: 0 },
|
|
114
|
+
];
|
|
115
|
+
let selectionsVisited = 0;
|
|
116
|
+
while (pending.length > 0) {
|
|
117
|
+
const current = pending.pop();
|
|
118
|
+
if (!current)
|
|
119
|
+
break;
|
|
120
|
+
if (current.depth > MAX_GRAPHQL_TRAVERSAL_DEPTH)
|
|
121
|
+
return null;
|
|
122
|
+
const selections = objectPairValue(current.selectionSet, 'selections');
|
|
123
|
+
const array = selections ? unwrapExpression(selections) : null;
|
|
124
|
+
if (!array || array.type !== 'array')
|
|
125
|
+
return null;
|
|
126
|
+
for (const item of array.namedChildren) {
|
|
127
|
+
selectionsVisited++;
|
|
128
|
+
if (selectionsVisited > MAX_GRAPHQL_SELECTIONS)
|
|
129
|
+
return null;
|
|
130
|
+
const selection = unwrapExpression(item);
|
|
131
|
+
const kind = literalValue(objectPairValue(selection, 'kind'));
|
|
132
|
+
if (kind === 'Field') {
|
|
133
|
+
const field = graphqlNameValue(objectPairValue(selection, 'name'));
|
|
134
|
+
if (field)
|
|
135
|
+
fields.add(field);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (kind === 'InlineFragment') {
|
|
139
|
+
const nested = objectPairValue(selection, 'selectionSet');
|
|
140
|
+
if (nested)
|
|
141
|
+
pending.push({ selectionSet: nested, depth: current.depth + 1 });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (kind !== 'FragmentSpread')
|
|
145
|
+
continue;
|
|
146
|
+
const name = graphqlNameValue(objectPairValue(selection, 'name'));
|
|
147
|
+
if (!name || seenFragments.has(name))
|
|
148
|
+
continue;
|
|
149
|
+
const fragment = fragments.get(name);
|
|
150
|
+
if (!fragment)
|
|
151
|
+
continue;
|
|
152
|
+
const nested = objectPairValue(fragment, 'selectionSet');
|
|
153
|
+
if (!nested)
|
|
154
|
+
continue;
|
|
155
|
+
seenFragments.add(name);
|
|
156
|
+
pending.push({ selectionSet: nested, depth: current.depth + 1 });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return fields;
|
|
160
|
+
}
|
|
161
|
+
function parsedDocumentProof(source, operationKind, operationName, requiredFields) {
|
|
162
|
+
let document;
|
|
163
|
+
try {
|
|
164
|
+
document = parse(source, { noLocation: true, maxTokens: MAX_GRAPHQL_TOKENS });
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
if (document.definitions.length > MAX_GRAPHQL_DEFINITIONS)
|
|
170
|
+
return false;
|
|
171
|
+
const fragments = new Map();
|
|
172
|
+
for (const definition of document.definitions) {
|
|
173
|
+
if (definition.kind === Kind.FRAGMENT_DEFINITION)
|
|
174
|
+
fragments.set(definition.name.value, definition);
|
|
175
|
+
}
|
|
176
|
+
for (const definition of document.definitions) {
|
|
177
|
+
if (definition.kind !== Kind.OPERATION_DEFINITION)
|
|
178
|
+
continue;
|
|
179
|
+
if (definition.operation !== operationKind || definition.name?.value !== operationName)
|
|
180
|
+
continue;
|
|
181
|
+
const fields = rootFields(definition.selectionSet, fragments);
|
|
182
|
+
return fields !== null && requiredFields.every((field) => fields.includes(field));
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
function staticGraphqlSource(initializer) {
|
|
187
|
+
const value = unwrapExpression(initializer);
|
|
188
|
+
if (value.type === 'string') {
|
|
189
|
+
if (value.text.startsWith('"')) {
|
|
190
|
+
try {
|
|
191
|
+
return JSON.parse(value.text);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return unquote(value.text);
|
|
198
|
+
}
|
|
199
|
+
if (value.type === 'template_string')
|
|
200
|
+
return unquote(value.text);
|
|
201
|
+
if (value.type === 'call_expression') {
|
|
202
|
+
const template = value.namedChildren.find((child) => child.type === 'template_string');
|
|
203
|
+
return template ? unquote(template.text) : null;
|
|
204
|
+
}
|
|
205
|
+
if (value.type !== 'new_expression')
|
|
206
|
+
return null;
|
|
207
|
+
const constructor = value.childForFieldName('constructor') ?? value.namedChildren[0];
|
|
208
|
+
if (!constructor || !constructor.text.endsWith('TypedDocumentString'))
|
|
209
|
+
return null;
|
|
210
|
+
const args = value.childForFieldName('arguments');
|
|
211
|
+
const first = args?.namedChildren[0];
|
|
212
|
+
return first ? staticGraphqlSource(first) : null;
|
|
213
|
+
}
|
|
214
|
+
export function hasGeneratedDocumentProof(initializer, operationKind, operationName, requiredFields) {
|
|
215
|
+
if (!withinGeneratedAstBudget(initializer))
|
|
216
|
+
return false;
|
|
217
|
+
const staticSource = staticGraphqlSource(initializer);
|
|
218
|
+
if (staticSource !== null) {
|
|
219
|
+
return parsedDocumentProof(staticSource, operationKind, operationName, requiredFields);
|
|
220
|
+
}
|
|
221
|
+
const document = unwrapExpression(initializer);
|
|
222
|
+
if (literalValue(objectPairValue(document, 'kind')) !== 'Document')
|
|
223
|
+
return false;
|
|
224
|
+
const definitions = objectPairValue(document, 'definitions');
|
|
225
|
+
const array = definitions ? unwrapExpression(definitions) : null;
|
|
226
|
+
if (!array || array.type !== 'array')
|
|
227
|
+
return false;
|
|
228
|
+
const fragments = new Map();
|
|
229
|
+
for (const item of array.namedChildren) {
|
|
230
|
+
const definition = unwrapExpression(item);
|
|
231
|
+
if (literalValue(objectPairValue(definition, 'kind')) !== 'FragmentDefinition')
|
|
232
|
+
continue;
|
|
233
|
+
const name = graphqlNameValue(objectPairValue(definition, 'name'));
|
|
234
|
+
if (name)
|
|
235
|
+
fragments.set(name, definition);
|
|
236
|
+
}
|
|
237
|
+
for (const item of array.namedChildren) {
|
|
238
|
+
const definition = unwrapExpression(item);
|
|
239
|
+
if (literalValue(objectPairValue(definition, 'kind')) !== 'OperationDefinition')
|
|
240
|
+
continue;
|
|
241
|
+
if (literalValue(objectPairValue(definition, 'operation')) !== operationKind)
|
|
242
|
+
continue;
|
|
243
|
+
if (graphqlNameValue(objectPairValue(definition, 'name')) !== operationName)
|
|
244
|
+
continue;
|
|
245
|
+
const fields = generatedRootFields(objectPairValue(definition, 'selectionSet'), fragments);
|
|
246
|
+
if (fields && requiredFields.every((field) => fields.has(field)))
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
function importedDecoratorBindings(root) {
|
|
252
|
+
const operations = new Map();
|
|
253
|
+
const resolvers = new Set();
|
|
254
|
+
for (const child of root.namedChildren) {
|
|
255
|
+
if (child.type !== 'import_statement')
|
|
256
|
+
continue;
|
|
257
|
+
const source = child.childForFieldName('source');
|
|
258
|
+
if (!source || unquote(source.text) !== NEST_GRAPHQL_PACKAGE)
|
|
259
|
+
continue;
|
|
260
|
+
const namedImports = child.namedChildren
|
|
261
|
+
.find((node) => node.type === 'import_clause')
|
|
262
|
+
?.namedChildren.find((node) => node.type === 'named_imports');
|
|
263
|
+
if (!namedImports)
|
|
264
|
+
continue;
|
|
265
|
+
for (const specifier of namedImports.namedChildren) {
|
|
266
|
+
if (specifier.type !== 'import_specifier')
|
|
267
|
+
continue;
|
|
268
|
+
const imported = specifier.childForFieldName('name')?.text;
|
|
269
|
+
const local = specifier.childForFieldName('alias')?.text ?? imported;
|
|
270
|
+
if (!imported || !local)
|
|
271
|
+
continue;
|
|
272
|
+
const kind = imported.toLowerCase();
|
|
273
|
+
if (kind === 'query' || kind === 'mutation' || kind === 'subscription') {
|
|
274
|
+
operations.set(local, kind);
|
|
275
|
+
}
|
|
276
|
+
else if (imported === 'Resolver') {
|
|
277
|
+
resolvers.add(local);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return { operations, resolvers };
|
|
282
|
+
}
|
|
283
|
+
function decoratorKind(decorator, bindings) {
|
|
284
|
+
const expression = decorator.namedChildren[0];
|
|
285
|
+
if (!expression)
|
|
286
|
+
return null;
|
|
287
|
+
if (expression.type === 'identifier') {
|
|
288
|
+
const kind = bindings.get(expression.text);
|
|
289
|
+
return kind ? { kind } : null;
|
|
290
|
+
}
|
|
291
|
+
if (expression.type !== 'call_expression')
|
|
292
|
+
return null;
|
|
293
|
+
const callee = expression.childForFieldName('function');
|
|
294
|
+
if (!callee || callee.type !== 'identifier')
|
|
295
|
+
return null;
|
|
296
|
+
const kind = bindings.get(callee.text);
|
|
297
|
+
if (!kind)
|
|
298
|
+
return null;
|
|
299
|
+
return { kind, argumentsNode: expression.childForFieldName('arguments') ?? undefined };
|
|
300
|
+
}
|
|
301
|
+
function decoratorFieldName(argumentsNode) {
|
|
302
|
+
if (!argumentsNode || argumentsNode.namedChildren.length === 0)
|
|
303
|
+
return { kind: 'absent' };
|
|
304
|
+
const args = argumentsNode.namedChildren;
|
|
305
|
+
if (args[0] && ['string', 'template_string'].includes(args[0].type)) {
|
|
306
|
+
const direct = unquote(args[0].text);
|
|
307
|
+
return direct === null ? { kind: 'dynamic' } : { kind: 'literal', value: direct };
|
|
308
|
+
}
|
|
309
|
+
let sawOptions = false;
|
|
310
|
+
for (const arg of args) {
|
|
311
|
+
if (arg.type !== 'object')
|
|
312
|
+
continue;
|
|
313
|
+
sawOptions = true;
|
|
314
|
+
for (const pair of arg.namedChildren) {
|
|
315
|
+
if (pair.type === 'spread_element' || pair.type.startsWith('shorthand_property_identifier')) {
|
|
316
|
+
return { kind: 'dynamic' };
|
|
317
|
+
}
|
|
318
|
+
if (pair.type !== 'pair')
|
|
319
|
+
continue;
|
|
320
|
+
const key = pair.childForFieldName('key')?.text.replace(/^['"]|['"]$/g, '');
|
|
321
|
+
if (key !== 'name')
|
|
322
|
+
continue;
|
|
323
|
+
const value = pair.childForFieldName('value');
|
|
324
|
+
if (!value || !['string', 'template_string'].includes(value.type)) {
|
|
325
|
+
return { kind: 'dynamic' };
|
|
326
|
+
}
|
|
327
|
+
const literal = unquote(value.text);
|
|
328
|
+
return literal === null ? { kind: 'dynamic' } : { kind: 'literal', value: literal };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (sawOptions || args.length === 1)
|
|
332
|
+
return { kind: 'absent' };
|
|
333
|
+
return { kind: 'dynamic' };
|
|
334
|
+
}
|
|
335
|
+
function topLevelResolverClassBodies(root, resolverBindings) {
|
|
336
|
+
if (!withinGeneratedAstBudget(root))
|
|
337
|
+
return null;
|
|
338
|
+
const bodies = [];
|
|
339
|
+
for (const statement of root.namedChildren) {
|
|
340
|
+
const classNode = statement.type === 'class_declaration'
|
|
341
|
+
? statement
|
|
342
|
+
: statement.type === 'export_statement'
|
|
343
|
+
? statement.namedChildren.find((child) => child.type === 'class_declaration')
|
|
344
|
+
: undefined;
|
|
345
|
+
if (!classNode)
|
|
346
|
+
continue;
|
|
347
|
+
const decorators = [
|
|
348
|
+
...new Set([
|
|
349
|
+
...statement.namedChildren.filter((child) => child.type === 'decorator'),
|
|
350
|
+
...classNode.namedChildren.filter((child) => child.type === 'decorator'),
|
|
351
|
+
]),
|
|
352
|
+
];
|
|
353
|
+
const isResolver = decorators.some((decorator) => {
|
|
354
|
+
const expression = decorator.namedChildren[0];
|
|
355
|
+
if (!expression)
|
|
356
|
+
return false;
|
|
357
|
+
const callee = expression.type === 'call_expression'
|
|
358
|
+
? expression.childForFieldName('function')
|
|
359
|
+
: expression;
|
|
360
|
+
return callee?.type === 'identifier' && resolverBindings.has(callee.text);
|
|
361
|
+
});
|
|
362
|
+
if (!isResolver)
|
|
363
|
+
continue;
|
|
364
|
+
const body = classNode.childForFieldName('body');
|
|
365
|
+
if (body)
|
|
366
|
+
bodies.push(body);
|
|
367
|
+
}
|
|
368
|
+
return bodies;
|
|
369
|
+
}
|
|
370
|
+
function rootFields(selectionSet, fragments) {
|
|
371
|
+
const fields = [];
|
|
372
|
+
const seenFragments = new Set();
|
|
373
|
+
const pending = [
|
|
374
|
+
{ selectionSet, depth: 0 },
|
|
375
|
+
];
|
|
376
|
+
let selectionsVisited = 0;
|
|
377
|
+
while (pending.length > 0) {
|
|
378
|
+
const current = pending.pop();
|
|
379
|
+
if (!current)
|
|
380
|
+
break;
|
|
381
|
+
if (current.depth > MAX_GRAPHQL_TRAVERSAL_DEPTH)
|
|
382
|
+
return null;
|
|
383
|
+
for (const selection of current.selectionSet.selections) {
|
|
384
|
+
selectionsVisited++;
|
|
385
|
+
if (selectionsVisited > MAX_GRAPHQL_SELECTIONS)
|
|
386
|
+
return null;
|
|
387
|
+
if (selection.kind === Kind.FIELD) {
|
|
388
|
+
fields.push(selection.name.value);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (selection.kind === Kind.INLINE_FRAGMENT) {
|
|
392
|
+
pending.push({ selectionSet: selection.selectionSet, depth: current.depth + 1 });
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const name = selection.name.value;
|
|
396
|
+
if (seenFragments.has(name))
|
|
397
|
+
continue;
|
|
398
|
+
const fragment = fragments.get(name);
|
|
399
|
+
if (!fragment)
|
|
400
|
+
continue;
|
|
401
|
+
seenFragments.add(name);
|
|
402
|
+
pending.push({ selectionSet: fragment.selectionSet, depth: current.depth + 1 });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return fields;
|
|
406
|
+
}
|
|
407
|
+
function generatedCandidates(operation) {
|
|
408
|
+
const name = operation.name?.value;
|
|
409
|
+
return name ? [`${name}Document`] : [];
|
|
410
|
+
}
|
|
411
|
+
async function generatedDocumentMatches(repoPath, symbol, operationKind, operationName, requiredFields, cache) {
|
|
412
|
+
const normalizedPath = symbol.filePath.replace(/\\/g, '/');
|
|
413
|
+
let pendingIndex = cache.get(normalizedPath);
|
|
414
|
+
if (!pendingIndex) {
|
|
415
|
+
pendingIndex = buildGeneratedSymbolIndex(repoPath, normalizedPath);
|
|
416
|
+
cache.set(normalizedPath, pendingIndex);
|
|
417
|
+
}
|
|
418
|
+
const index = await pendingIndex;
|
|
419
|
+
const values = index?.get(symbol.name) ?? [];
|
|
420
|
+
return values.some((value) => hasGeneratedDocumentProof(value, operationKind, operationName, requiredFields));
|
|
421
|
+
}
|
|
422
|
+
async function buildGeneratedSymbolIndex(repoPath, filePath) {
|
|
423
|
+
const source = await readSafeBounded(repoPath, filePath, getMaxFileSizeBytes());
|
|
424
|
+
if (source === null)
|
|
425
|
+
return null;
|
|
426
|
+
const parser = new Parser();
|
|
427
|
+
parser.setLanguage(filePath.toLowerCase().endsWith('.tsx') ? TypeScript.tsx : TypeScript.typescript);
|
|
428
|
+
let tree;
|
|
429
|
+
try {
|
|
430
|
+
tree = parseSourceSafe(parser, source, undefined, undefined, filePath);
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
if (error instanceof ParseTimeoutError)
|
|
434
|
+
return null;
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
return indexGeneratedDeclarators(tree.rootNode);
|
|
438
|
+
}
|
|
439
|
+
export function indexGeneratedDeclarators(root) {
|
|
440
|
+
const index = new Map();
|
|
441
|
+
const pending = [root];
|
|
442
|
+
while (pending.length > 0) {
|
|
443
|
+
const node = pending.pop();
|
|
444
|
+
if (!node)
|
|
445
|
+
break;
|
|
446
|
+
if (node.type === 'variable_declarator') {
|
|
447
|
+
const name = node.childForFieldName('name')?.text;
|
|
448
|
+
const value = node.childForFieldName('value');
|
|
449
|
+
if (name && value) {
|
|
450
|
+
const values = index.get(name) ?? [];
|
|
451
|
+
values.push(value);
|
|
452
|
+
index.set(name, values);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
for (let child = node.namedChildren.length - 1; child >= 0; child--) {
|
|
456
|
+
pending.push(node.namedChildren[child]);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return index;
|
|
460
|
+
}
|
|
461
|
+
function dedupe(contracts) {
|
|
462
|
+
const seen = new Set();
|
|
463
|
+
return contracts.filter((contract) => {
|
|
464
|
+
const key = `${contract.contractId}|${contract.role}|${contract.symbolUid}`;
|
|
465
|
+
if (seen.has(key))
|
|
466
|
+
return false;
|
|
467
|
+
seen.add(key);
|
|
468
|
+
return true;
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
export class GraphqlExtractor {
|
|
472
|
+
type = 'graphql';
|
|
473
|
+
async canExtract(_repo) {
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
async extract(dbExecutor, repoPath, _repo) {
|
|
477
|
+
if (!dbExecutor)
|
|
478
|
+
return [];
|
|
479
|
+
const ignore = await createIgnoreFilter(repoPath);
|
|
480
|
+
const [providerFiles, documentFiles] = await Promise.all([
|
|
481
|
+
glob(PROVIDER_GLOB, { cwd: repoPath, ignore, nodir: true }),
|
|
482
|
+
glob(DOCUMENT_GLOB, { cwd: repoPath, ignore, nodir: true }),
|
|
483
|
+
]);
|
|
484
|
+
const contracts = [
|
|
485
|
+
...(await this.extractProviders(dbExecutor, repoPath, providerFiles)),
|
|
486
|
+
...(await this.extractConsumers(dbExecutor, repoPath, documentFiles)),
|
|
487
|
+
];
|
|
488
|
+
return dedupe(contracts);
|
|
489
|
+
}
|
|
490
|
+
async extractProviders(dbExecutor, repoPath, files) {
|
|
491
|
+
const parser = new Parser();
|
|
492
|
+
const contracts = [];
|
|
493
|
+
const maxFileSizeBytes = getMaxFileSizeBytes();
|
|
494
|
+
for (const rel of files) {
|
|
495
|
+
if (/\.(?:spec|test)\.[cm]?tsx?$/i.test(rel))
|
|
496
|
+
continue;
|
|
497
|
+
const source = await readSafeBounded(repoPath, rel, maxFileSizeBytes);
|
|
498
|
+
if (source === null || !source.includes(NEST_GRAPHQL_PACKAGE))
|
|
499
|
+
continue;
|
|
500
|
+
parser.setLanguage(rel.toLowerCase().endsWith('.tsx') ? TypeScript.tsx : TypeScript.typescript);
|
|
501
|
+
let tree;
|
|
502
|
+
try {
|
|
503
|
+
tree = parseSourceSafe(parser, source, undefined, undefined, rel);
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
if (error instanceof ParseTimeoutError)
|
|
507
|
+
continue;
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
const bindings = importedDecoratorBindings(tree.rootNode);
|
|
511
|
+
if (bindings.operations.size === 0 || bindings.resolvers.size === 0)
|
|
512
|
+
continue;
|
|
513
|
+
const bodies = topLevelResolverClassBodies(tree.rootNode, bindings.resolvers);
|
|
514
|
+
if (bodies === null)
|
|
515
|
+
continue;
|
|
516
|
+
for (const body of bodies) {
|
|
517
|
+
let decorators = [];
|
|
518
|
+
for (const member of body.namedChildren) {
|
|
519
|
+
if (member.type === 'comment')
|
|
520
|
+
continue;
|
|
521
|
+
if (member.type === 'decorator') {
|
|
522
|
+
decorators.push(member);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (member.type !== 'method_definition' && member.type !== 'public_field_definition') {
|
|
526
|
+
decorators = [];
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const memberDecorators = [
|
|
530
|
+
...new Set([
|
|
531
|
+
...decorators,
|
|
532
|
+
...member.namedChildren.filter((child) => child.type === 'decorator'),
|
|
533
|
+
]),
|
|
534
|
+
];
|
|
535
|
+
const methodName = member.childForFieldName('name')?.text;
|
|
536
|
+
if (!methodName) {
|
|
537
|
+
decorators = [];
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
for (const decorator of memberDecorators) {
|
|
541
|
+
const operation = decoratorKind(decorator, bindings.operations);
|
|
542
|
+
if (!operation)
|
|
543
|
+
continue;
|
|
544
|
+
const parsedField = decoratorFieldName(operation.argumentsNode);
|
|
545
|
+
if (parsedField.kind === 'dynamic')
|
|
546
|
+
continue;
|
|
547
|
+
const field = parsedField.kind === 'literal' ? parsedField.value : methodName;
|
|
548
|
+
if (!GRAPHQL_NAME.test(field))
|
|
549
|
+
continue;
|
|
550
|
+
const filePath = rel.replace(/\\/g, '/');
|
|
551
|
+
const symbol = uniqueRealSymbol(await dbExecutor(RESOLVE_METHOD_QUERY, {
|
|
552
|
+
name: methodName,
|
|
553
|
+
filePath,
|
|
554
|
+
startLine: member.type === 'public_field_definition'
|
|
555
|
+
? (member.childForFieldName('value')?.startPosition.row ??
|
|
556
|
+
member.startPosition.row) + 1
|
|
557
|
+
: member.startPosition.row + 1,
|
|
558
|
+
}));
|
|
559
|
+
if (!symbol)
|
|
560
|
+
continue;
|
|
561
|
+
contracts.push({
|
|
562
|
+
contractId: `graphql::${operation.kind}::${field}`,
|
|
563
|
+
type: 'graphql',
|
|
564
|
+
role: 'provider',
|
|
565
|
+
symbolUid: symbol.uid,
|
|
566
|
+
symbolRef: { filePath: symbol.filePath, name: symbol.name },
|
|
567
|
+
symbolName: symbol.name,
|
|
568
|
+
confidence: 1,
|
|
569
|
+
meta: {
|
|
570
|
+
operationKind: operation.kind,
|
|
571
|
+
fieldName: field,
|
|
572
|
+
resolverPath: filePath,
|
|
573
|
+
extractionStrategy: 'nestjs_decorator',
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
decorators = [];
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return contracts;
|
|
582
|
+
}
|
|
583
|
+
async extractConsumers(dbExecutor, repoPath, files) {
|
|
584
|
+
const contracts = [];
|
|
585
|
+
const generatedIndexCache = new Map();
|
|
586
|
+
const maxFileSizeBytes = getMaxFileSizeBytes();
|
|
587
|
+
for (const rel of files) {
|
|
588
|
+
const source = await readSafeBounded(repoPath, rel, maxFileSizeBytes);
|
|
589
|
+
if (source === null)
|
|
590
|
+
continue;
|
|
591
|
+
let document;
|
|
592
|
+
try {
|
|
593
|
+
document = parse(source, { noLocation: true, maxTokens: MAX_GRAPHQL_TOKENS });
|
|
594
|
+
}
|
|
595
|
+
catch (error) {
|
|
596
|
+
logger.debug({ file: rel, error }, 'skipping invalid GraphQL document');
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (document.definitions.length > MAX_GRAPHQL_DEFINITIONS)
|
|
600
|
+
continue;
|
|
601
|
+
const fragments = new Map();
|
|
602
|
+
for (const definition of document.definitions) {
|
|
603
|
+
if (definition.kind === Kind.FRAGMENT_DEFINITION) {
|
|
604
|
+
fragments.set(definition.name.value, definition);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const operations = document.definitions.filter((definition) => definition.kind === Kind.OPERATION_DEFINITION && definition.name !== undefined);
|
|
608
|
+
if (operations.length > MAX_GRAPHQL_OPERATIONS)
|
|
609
|
+
continue;
|
|
610
|
+
for (const definition of operations) {
|
|
611
|
+
const operationName = definition.name?.value;
|
|
612
|
+
if (!operationName)
|
|
613
|
+
continue;
|
|
614
|
+
const documentPath = rel.replace(/\\/g, '/');
|
|
615
|
+
const operationFields = rootFields(definition.selectionSet, fragments);
|
|
616
|
+
if (operationFields === null)
|
|
617
|
+
continue;
|
|
618
|
+
const uniqueFields = [...new Set(operationFields)];
|
|
619
|
+
let symbol = null;
|
|
620
|
+
for (const candidate of generatedCandidates(definition)) {
|
|
621
|
+
const resolved = uniqueRealSymbol(await dbExecutor(RESOLVE_GENERATED_SYMBOL_QUERY, { name: candidate }));
|
|
622
|
+
if (resolved &&
|
|
623
|
+
(await generatedDocumentMatches(repoPath, resolved, definition.operation, operationName, uniqueFields, generatedIndexCache))) {
|
|
624
|
+
symbol = resolved;
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (!symbol)
|
|
629
|
+
continue;
|
|
630
|
+
for (const field of uniqueFields) {
|
|
631
|
+
contracts.push({
|
|
632
|
+
contractId: `graphql::${definition.operation}::${field}`,
|
|
633
|
+
type: 'graphql',
|
|
634
|
+
role: 'consumer',
|
|
635
|
+
symbolUid: symbol.uid,
|
|
636
|
+
symbolRef: { filePath: symbol.filePath, name: symbol.name },
|
|
637
|
+
symbolName: symbol.name,
|
|
638
|
+
confidence: 1,
|
|
639
|
+
meta: {
|
|
640
|
+
operationKind: definition.operation,
|
|
641
|
+
operationName: definition.name.value,
|
|
642
|
+
fieldName: field,
|
|
643
|
+
documentPath,
|
|
644
|
+
extractionStrategy: 'graphql_ast',
|
|
645
|
+
},
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return contracts;
|
|
651
|
+
}
|
|
652
|
+
}
|