gitnexus 1.6.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/analyze.js +28 -3
- package/dist/core/group/extractors/fs-utils.d.ts +10 -0
- package/dist/core/group/extractors/fs-utils.js +24 -0
- package/dist/core/group/extractors/grpc-extractor.d.ts +17 -8
- package/dist/core/group/extractors/grpc-extractor.js +313 -191
- package/dist/core/group/extractors/grpc-patterns/go.d.ts +2 -0
- package/dist/core/group/extractors/grpc-patterns/go.js +97 -0
- package/dist/core/group/extractors/grpc-patterns/index.d.ts +19 -0
- package/dist/core/group/extractors/grpc-patterns/index.js +46 -0
- package/dist/core/group/extractors/grpc-patterns/java.d.ts +2 -0
- package/dist/core/group/extractors/grpc-patterns/java.js +173 -0
- package/dist/core/group/extractors/grpc-patterns/node.d.ts +4 -0
- package/dist/core/group/extractors/grpc-patterns/node.js +290 -0
- package/dist/core/group/extractors/grpc-patterns/proto.d.ts +9 -0
- package/dist/core/group/extractors/grpc-patterns/proto.js +134 -0
- package/dist/core/group/extractors/grpc-patterns/python.d.ts +2 -0
- package/dist/core/group/extractors/grpc-patterns/python.js +67 -0
- package/dist/core/group/extractors/grpc-patterns/types.d.ts +50 -0
- package/dist/core/group/extractors/grpc-patterns/types.js +1 -0
- package/dist/core/group/extractors/http-patterns/go.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/go.js +215 -0
- package/dist/core/group/extractors/http-patterns/index.d.ts +17 -0
- package/dist/core/group/extractors/http-patterns/index.js +44 -0
- package/dist/core/group/extractors/http-patterns/java.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/java.js +253 -0
- package/dist/core/group/extractors/http-patterns/node.d.ts +4 -0
- package/dist/core/group/extractors/http-patterns/node.js +354 -0
- package/dist/core/group/extractors/http-patterns/php.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/php.js +70 -0
- package/dist/core/group/extractors/http-patterns/python.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/python.js +133 -0
- package/dist/core/group/extractors/http-patterns/types.d.ts +61 -0
- package/dist/core/group/extractors/http-patterns/types.js +1 -0
- package/dist/core/group/extractors/http-route-extractor.d.ts +10 -13
- package/dist/core/group/extractors/http-route-extractor.js +201 -238
- package/dist/core/group/extractors/manifest-extractor.d.ts +54 -0
- package/dist/core/group/extractors/manifest-extractor.js +235 -0
- package/dist/core/group/extractors/topic-extractor.d.ts +0 -1
- package/dist/core/group/extractors/topic-extractor.js +55 -192
- package/dist/core/group/extractors/topic-patterns/go.d.ts +2 -0
- package/dist/core/group/extractors/topic-patterns/go.js +120 -0
- package/dist/core/group/extractors/topic-patterns/index.d.ts +14 -0
- package/dist/core/group/extractors/topic-patterns/index.js +38 -0
- package/dist/core/group/extractors/topic-patterns/java.d.ts +2 -0
- package/dist/core/group/extractors/topic-patterns/java.js +80 -0
- package/dist/core/group/extractors/topic-patterns/node.d.ts +4 -0
- package/dist/core/group/extractors/topic-patterns/node.js +155 -0
- package/dist/core/group/extractors/topic-patterns/python.d.ts +2 -0
- package/dist/core/group/extractors/topic-patterns/python.js +116 -0
- package/dist/core/group/extractors/topic-patterns/types.d.ts +25 -0
- package/dist/core/group/extractors/topic-patterns/types.js +10 -0
- package/dist/core/group/extractors/tree-sitter-scanner.d.ts +113 -0
- package/dist/core/group/extractors/tree-sitter-scanner.js +94 -0
- package/dist/core/ingestion/binding-accumulator.d.ts +22 -17
- package/dist/core/ingestion/binding-accumulator.js +29 -25
- package/dist/core/ingestion/cobol-processor.d.ts +1 -1
- package/dist/core/ingestion/import-processor.js +1 -1
- package/dist/core/ingestion/language-config.js +1 -1
- package/dist/core/ingestion/language-provider.d.ts +8 -0
- package/dist/core/ingestion/languages/ruby.js +15 -0
- package/dist/core/ingestion/markdown-processor.d.ts +1 -1
- package/dist/core/ingestion/method-extractors/configs/jvm.js +1 -0
- package/dist/core/ingestion/method-extractors/configs/ruby.js +1 -0
- package/dist/core/ingestion/method-extractors/generic.d.ts +6 -0
- package/dist/core/ingestion/method-extractors/generic.js +48 -4
- package/dist/core/ingestion/method-types.d.ts +4 -0
- package/dist/core/ingestion/model/resolve.js +103 -48
- package/dist/core/ingestion/model/semantic-model.d.ts +1 -1
- package/dist/core/ingestion/model/semantic-model.js +1 -1
- package/dist/core/ingestion/model/symbol-table.d.ts +7 -7
- package/dist/core/ingestion/model/symbol-table.js +7 -7
- package/dist/core/ingestion/mro-processor.d.ts +1 -1
- package/dist/core/ingestion/mro-processor.js +1 -1
- package/dist/core/ingestion/parsing-processor.js +54 -42
- package/dist/core/ingestion/pipeline-phases/cobol.d.ts +16 -0
- package/dist/core/ingestion/pipeline-phases/cobol.js +45 -0
- package/dist/core/ingestion/pipeline-phases/communities.d.ts +16 -0
- package/dist/core/ingestion/pipeline-phases/communities.js +62 -0
- package/dist/core/ingestion/pipeline-phases/cross-file-impl.d.ts +17 -0
- package/dist/core/ingestion/pipeline-phases/cross-file-impl.js +156 -0
- package/dist/core/ingestion/pipeline-phases/cross-file.d.ts +37 -0
- package/dist/core/ingestion/pipeline-phases/cross-file.js +63 -0
- package/dist/core/ingestion/pipeline-phases/index.d.ts +21 -0
- package/dist/core/ingestion/pipeline-phases/index.js +22 -0
- package/dist/core/ingestion/pipeline-phases/markdown.d.ts +17 -0
- package/dist/core/ingestion/pipeline-phases/markdown.js +33 -0
- package/dist/core/ingestion/pipeline-phases/mro.d.ts +18 -0
- package/dist/core/ingestion/pipeline-phases/mro.js +36 -0
- package/dist/core/ingestion/pipeline-phases/orm-extraction.d.ts +22 -0
- package/dist/core/ingestion/pipeline-phases/orm-extraction.js +92 -0
- package/dist/core/ingestion/pipeline-phases/orm.d.ts +15 -0
- package/dist/core/ingestion/pipeline-phases/orm.js +74 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +47 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +437 -0
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +49 -0
- package/dist/core/ingestion/pipeline-phases/parse.js +33 -0
- package/dist/core/ingestion/pipeline-phases/processes.d.ts +16 -0
- package/dist/core/ingestion/pipeline-phases/processes.js +143 -0
- package/dist/core/ingestion/pipeline-phases/routes.d.ts +21 -0
- package/dist/core/ingestion/pipeline-phases/routes.js +243 -0
- package/dist/core/ingestion/pipeline-phases/runner.d.ts +22 -0
- package/dist/core/ingestion/pipeline-phases/runner.js +203 -0
- package/dist/core/ingestion/pipeline-phases/scan.d.ts +21 -0
- package/dist/core/ingestion/pipeline-phases/scan.js +46 -0
- package/dist/core/ingestion/pipeline-phases/structure.d.ts +27 -0
- package/dist/core/ingestion/pipeline-phases/structure.js +35 -0
- package/dist/core/ingestion/pipeline-phases/tools.d.ts +20 -0
- package/dist/core/ingestion/pipeline-phases/tools.js +79 -0
- package/dist/core/ingestion/pipeline-phases/types.d.ts +79 -0
- package/dist/core/ingestion/pipeline-phases/types.js +37 -0
- package/dist/core/ingestion/pipeline-phases/wildcard-synthesis.d.ts +35 -0
- package/dist/core/ingestion/pipeline-phases/wildcard-synthesis.js +174 -0
- package/dist/core/ingestion/pipeline.d.ts +16 -10
- package/dist/core/ingestion/pipeline.js +66 -1534
- package/dist/core/ingestion/process-processor.js +1 -1
- package/dist/core/ingestion/tree-sitter-queries.d.ts +2 -2
- package/dist/core/ingestion/tree-sitter-queries.js +69 -0
- package/dist/core/ingestion/utils/ast-helpers.d.ts +1 -3
- package/dist/core/ingestion/utils/ast-helpers.js +48 -21
- package/dist/core/ingestion/utils/env.d.ts +10 -0
- package/dist/core/ingestion/utils/env.js +10 -0
- package/dist/core/ingestion/utils/graph-sort.d.ts +58 -0
- package/dist/core/ingestion/utils/graph-sort.js +100 -0
- package/dist/core/ingestion/workers/parse-worker.js +12 -8
- package/dist/core/lbug/lbug-adapter.js +66 -24
- package/package.json +3 -3
- package/vendor/tree-sitter-proto/binding.gyp +30 -0
- package/vendor/tree-sitter-proto/bindings/node/binding.cc +20 -0
- package/vendor/tree-sitter-proto/bindings/node/index.d.ts +28 -0
- package/vendor/tree-sitter-proto/bindings/node/index.js +7 -0
- package/vendor/tree-sitter-proto/package.json +18 -0
- package/vendor/tree-sitter-proto/src/node-types.json +1145 -0
- package/vendor/tree-sitter-proto/src/parser.c +10149 -0
- package/vendor/tree-sitter-proto/src/tree_sitter/alloc.h +54 -0
- package/vendor/tree-sitter-proto/src/tree_sitter/array.h +291 -0
- package/vendor/tree-sitter-proto/src/tree_sitter/parser.h +266 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { GO_GRPC_PLUGIN } from './go.js';
|
|
3
|
+
import { JAVA_GRPC_PLUGIN } from './java.js';
|
|
4
|
+
import { PYTHON_GRPC_PLUGIN } from './python.js';
|
|
5
|
+
import { JAVASCRIPT_GRPC_PLUGIN, TYPESCRIPT_GRPC_PLUGIN, TSX_GRPC_PLUGIN } from './node.js';
|
|
6
|
+
import { PROTO_GRPC_PLUGIN } from './proto.js';
|
|
7
|
+
export { PROTO_GRPC_PLUGIN, extractPackageFromTree } from './proto.js';
|
|
8
|
+
/**
|
|
9
|
+
* File-extension → gRPC language plugin registry. Mirrors the shape
|
|
10
|
+
* of `http-patterns/index.ts` and `topic-patterns/index.ts`.
|
|
11
|
+
*
|
|
12
|
+
* `.proto` files are registered only when `tree-sitter-proto` is
|
|
13
|
+
* available (it's an optionalDependency). When absent, the orchestrator
|
|
14
|
+
* falls back to the built-in manual proto parser.
|
|
15
|
+
*/
|
|
16
|
+
const REGISTRY = {
|
|
17
|
+
'.go': GO_GRPC_PLUGIN,
|
|
18
|
+
'.java': JAVA_GRPC_PLUGIN,
|
|
19
|
+
'.py': PYTHON_GRPC_PLUGIN,
|
|
20
|
+
'.js': JAVASCRIPT_GRPC_PLUGIN,
|
|
21
|
+
'.jsx': JAVASCRIPT_GRPC_PLUGIN,
|
|
22
|
+
'.ts': TYPESCRIPT_GRPC_PLUGIN,
|
|
23
|
+
'.tsx': TSX_GRPC_PLUGIN,
|
|
24
|
+
...(PROTO_GRPC_PLUGIN ? { '.proto': PROTO_GRPC_PLUGIN } : {}),
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Glob for source files worth scanning for gRPC server/client patterns.
|
|
28
|
+
* Includes `.proto` when the grammar is available.
|
|
29
|
+
*/
|
|
30
|
+
export const GRPC_SCAN_GLOB = PROTO_GRPC_PLUGIN
|
|
31
|
+
? '**/*.{go,java,py,ts,tsx,js,jsx,proto}'
|
|
32
|
+
: '**/*.{go,java,py,ts,tsx,js,jsx}';
|
|
33
|
+
/**
|
|
34
|
+
* Whether the tree-sitter proto plugin is available. The orchestrator
|
|
35
|
+
* uses this to decide between the tree-sitter path and the fallback
|
|
36
|
+
* manual parser for `.proto` files.
|
|
37
|
+
*/
|
|
38
|
+
export const hasProtoPlugin = PROTO_GRPC_PLUGIN !== null;
|
|
39
|
+
/**
|
|
40
|
+
* Return the gRPC plugin registered for the given file's extension,
|
|
41
|
+
* or `undefined` if the extension is not registered.
|
|
42
|
+
*/
|
|
43
|
+
export function getPluginForFile(rel) {
|
|
44
|
+
const ext = path.extname(rel).toLowerCase();
|
|
45
|
+
return REGISTRY[ext];
|
|
46
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import Java from 'tree-sitter-java';
|
|
2
|
+
import { compilePatterns, runCompiledPatterns, } from '../tree-sitter-scanner.js';
|
|
3
|
+
/**
|
|
4
|
+
* Java gRPC plugin. Detects:
|
|
5
|
+
* - Provider: classes extending `XxxServiceGrpc.XxxServiceImplBase`
|
|
6
|
+
* (with or without a `@GrpcService` annotation; the annotation
|
|
7
|
+
* only affects confidence labelling in the original regex version
|
|
8
|
+
* — here we emit a single detection per class and pick the source
|
|
9
|
+
* label based on whether the annotation is present).
|
|
10
|
+
* - Consumer: `XxxServiceGrpc.newBlockingStub(ch)` /
|
|
11
|
+
* `XxxServiceGrpc.newStub(ch)` calls.
|
|
12
|
+
*/
|
|
13
|
+
const IMPL_BASE_RE = /^(\w+)ImplBase$/;
|
|
14
|
+
const GRPC_SUFFIX_RE = /^(\w+)Grpc$/;
|
|
15
|
+
// Classes extending `ScopedType.ScopedType` where the inner name ends
|
|
16
|
+
// in ImplBase. Covers `XxxServiceGrpc.XxxServiceImplBase`.
|
|
17
|
+
// Note: tree-sitter-java's `scoped_type_identifier` exposes its two
|
|
18
|
+
// segments as positional `type_identifier` children, NOT as named
|
|
19
|
+
// `scope:`/`name:` fields. We match positionally here and rely on the
|
|
20
|
+
// grammar's left-to-right ordering: first child = outer, second = inner.
|
|
21
|
+
const SCOPED_IMPL_BASE_PATTERNS = compilePatterns({
|
|
22
|
+
name: 'java-grpc-scoped-impl-base',
|
|
23
|
+
language: Java,
|
|
24
|
+
patterns: [
|
|
25
|
+
{
|
|
26
|
+
meta: {},
|
|
27
|
+
query: `
|
|
28
|
+
(class_declaration
|
|
29
|
+
name: (identifier) @class_name
|
|
30
|
+
superclass: (superclass
|
|
31
|
+
(scoped_type_identifier
|
|
32
|
+
(type_identifier) @outer
|
|
33
|
+
(type_identifier) @inner (#match? @inner "ImplBase$")))) @class
|
|
34
|
+
`,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
// Classes extending a simple `XxxImplBase` identifier (no scope).
|
|
39
|
+
const PLAIN_IMPL_BASE_PATTERNS = compilePatterns({
|
|
40
|
+
name: 'java-grpc-plain-impl-base',
|
|
41
|
+
language: Java,
|
|
42
|
+
patterns: [
|
|
43
|
+
{
|
|
44
|
+
meta: {},
|
|
45
|
+
query: `
|
|
46
|
+
(class_declaration
|
|
47
|
+
name: (identifier) @class_name
|
|
48
|
+
superclass: (superclass
|
|
49
|
+
(type_identifier) @plain_type (#match? @plain_type "ImplBase$"))) @class
|
|
50
|
+
`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
// gRPC stub factories: `XxxGrpc.newStub(ch)` / `XxxGrpc.newBlockingStub(ch)`.
|
|
55
|
+
const STUB_PATTERNS = compilePatterns({
|
|
56
|
+
name: 'java-grpc-stub',
|
|
57
|
+
language: Java,
|
|
58
|
+
patterns: [
|
|
59
|
+
{
|
|
60
|
+
meta: {},
|
|
61
|
+
query: `
|
|
62
|
+
(method_invocation
|
|
63
|
+
object: (identifier) @grpc_cls
|
|
64
|
+
name: (identifier) @method (#match? @method "^new(Blocking)?Stub$"))
|
|
65
|
+
`,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
/**
|
|
70
|
+
* Check whether a `class_declaration` node has a `@GrpcService`
|
|
71
|
+
* annotation in its modifiers list. In tree-sitter-java, class-level
|
|
72
|
+
* annotations live under `(class_declaration (modifiers (marker_annotation|annotation)))`.
|
|
73
|
+
*/
|
|
74
|
+
function hasGrpcServiceAnnotation(classNode) {
|
|
75
|
+
for (let i = 0; i < classNode.namedChildCount; i++) {
|
|
76
|
+
const child = classNode.namedChild(i);
|
|
77
|
+
if (!child || child.type !== 'modifiers')
|
|
78
|
+
continue;
|
|
79
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
80
|
+
const mod = child.namedChild(j);
|
|
81
|
+
if (!mod)
|
|
82
|
+
continue;
|
|
83
|
+
if (mod.type !== 'marker_annotation' && mod.type !== 'annotation')
|
|
84
|
+
continue;
|
|
85
|
+
const nameNode = mod.childForFieldName('name');
|
|
86
|
+
if (nameNode?.text === 'GrpcService')
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Given the inner type_identifier text like `AuthServiceImplBase`,
|
|
94
|
+
* return the service name (`AuthService`), or null if the text
|
|
95
|
+
* doesn't end in `ImplBase`.
|
|
96
|
+
*/
|
|
97
|
+
function extractServiceFromImplBase(text) {
|
|
98
|
+
const m = IMPL_BASE_RE.exec(text);
|
|
99
|
+
if (!m)
|
|
100
|
+
return null;
|
|
101
|
+
// Strip a trailing `Grpc` on the service name too — the original
|
|
102
|
+
// regex replaces `Grpc$` on the extracted prefix.
|
|
103
|
+
return m[1].replace(/Grpc$/, '');
|
|
104
|
+
}
|
|
105
|
+
export const JAVA_GRPC_PLUGIN = {
|
|
106
|
+
name: 'java-grpc',
|
|
107
|
+
language: Java,
|
|
108
|
+
scan(tree) {
|
|
109
|
+
const out = [];
|
|
110
|
+
const emittedClassIds = new Set();
|
|
111
|
+
// ─── Providers: scoped form (`...Grpc.XxxImplBase`) ─────────────
|
|
112
|
+
for (const match of runCompiledPatterns(SCOPED_IMPL_BASE_PATTERNS, tree)) {
|
|
113
|
+
const classNode = match.captures.class;
|
|
114
|
+
const innerNode = match.captures.inner;
|
|
115
|
+
if (!classNode || !innerNode)
|
|
116
|
+
continue;
|
|
117
|
+
const serviceName = extractServiceFromImplBase(innerNode.text);
|
|
118
|
+
if (!serviceName)
|
|
119
|
+
continue;
|
|
120
|
+
emittedClassIds.add(classNode.id);
|
|
121
|
+
const annotated = hasGrpcServiceAnnotation(classNode);
|
|
122
|
+
out.push({
|
|
123
|
+
role: 'provider',
|
|
124
|
+
serviceName,
|
|
125
|
+
symbolName: serviceName,
|
|
126
|
+
source: annotated ? 'java_grpc_service' : 'java_impl_base',
|
|
127
|
+
confidenceWithProto: 0.8,
|
|
128
|
+
confidenceWithoutProto: 0.65,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// ─── Providers: plain form (`XxxImplBase`) ──────────────────────
|
|
132
|
+
for (const match of runCompiledPatterns(PLAIN_IMPL_BASE_PATTERNS, tree)) {
|
|
133
|
+
const classNode = match.captures.class;
|
|
134
|
+
const plainNode = match.captures.plain_type;
|
|
135
|
+
if (!classNode || !plainNode)
|
|
136
|
+
continue;
|
|
137
|
+
if (emittedClassIds.has(classNode.id))
|
|
138
|
+
continue;
|
|
139
|
+
const serviceName = extractServiceFromImplBase(plainNode.text);
|
|
140
|
+
if (!serviceName)
|
|
141
|
+
continue;
|
|
142
|
+
emittedClassIds.add(classNode.id);
|
|
143
|
+
const annotated = hasGrpcServiceAnnotation(classNode);
|
|
144
|
+
out.push({
|
|
145
|
+
role: 'provider',
|
|
146
|
+
serviceName,
|
|
147
|
+
symbolName: serviceName,
|
|
148
|
+
source: annotated ? 'java_grpc_service' : 'java_impl_base',
|
|
149
|
+
confidenceWithProto: 0.8,
|
|
150
|
+
confidenceWithoutProto: 0.65,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// ─── Consumers: `XxxGrpc.newBlockingStub(...)` / `newStub(...)` ─
|
|
154
|
+
for (const match of runCompiledPatterns(STUB_PATTERNS, tree)) {
|
|
155
|
+
const grpcClsNode = match.captures.grpc_cls;
|
|
156
|
+
if (!grpcClsNode)
|
|
157
|
+
continue;
|
|
158
|
+
const grpcMatch = GRPC_SUFFIX_RE.exec(grpcClsNode.text);
|
|
159
|
+
if (!grpcMatch)
|
|
160
|
+
continue;
|
|
161
|
+
const serviceName = grpcMatch[1];
|
|
162
|
+
out.push({
|
|
163
|
+
role: 'consumer',
|
|
164
|
+
serviceName,
|
|
165
|
+
symbolName: `${serviceName}Stub`,
|
|
166
|
+
source: 'java_stub',
|
|
167
|
+
confidenceWithProto: 0.75,
|
|
168
|
+
confidenceWithoutProto: 0.55,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
},
|
|
173
|
+
};
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import JavaScript from 'tree-sitter-javascript';
|
|
2
|
+
import TypeScript from 'tree-sitter-typescript';
|
|
3
|
+
import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
|
|
4
|
+
/**
|
|
5
|
+
* Node.js / TypeScript gRPC plugin family. Detects:
|
|
6
|
+
* - Provider: NestJS `@GrpcMethod('Service', 'Method')` decorators
|
|
7
|
+
* - Consumer: NestJS `@GrpcClient(...) readonly x!: XxxServiceClient`
|
|
8
|
+
* - Consumer: `client.getService<X>('AuthService')`
|
|
9
|
+
* - Consumer: `new XxxServiceClient(...)` (generated client constructor)
|
|
10
|
+
* - Consumer: `new foo.bar.Xxx(...)` when the file uses
|
|
11
|
+
* `loadPackageDefinition` (gRPC dynamic proto loader)
|
|
12
|
+
*
|
|
13
|
+
* As with the HTTP `node.ts`, pattern sources are defined once and
|
|
14
|
+
* compiled against three grammar variants (JS / TS / TSX) because
|
|
15
|
+
* `Parser.Query` is not portable across grammar objects.
|
|
16
|
+
*/
|
|
17
|
+
const SERVICE_CLIENT_RE = /^(\w+Service)Client$/;
|
|
18
|
+
const CAPITALIZED_SERVICE_RE = /^[A-Z]\w+$/;
|
|
19
|
+
// @GrpcMethod('Service', 'Method')
|
|
20
|
+
const GRPC_METHOD_SPEC = {
|
|
21
|
+
meta: {},
|
|
22
|
+
query: `
|
|
23
|
+
(decorator
|
|
24
|
+
(call_expression
|
|
25
|
+
function: (identifier) @dec (#eq? @dec "GrpcMethod")
|
|
26
|
+
arguments: (arguments
|
|
27
|
+
. [(string) (template_string)] @service
|
|
28
|
+
. [(string) (template_string)] @method)))
|
|
29
|
+
`,
|
|
30
|
+
};
|
|
31
|
+
// @GrpcClient(...) standalone decorator — the plugin walks to the next
|
|
32
|
+
// sibling (a field definition) to read its type annotation.
|
|
33
|
+
const GRPC_CLIENT_SPEC = {
|
|
34
|
+
meta: {},
|
|
35
|
+
query: `
|
|
36
|
+
(decorator
|
|
37
|
+
(call_expression
|
|
38
|
+
function: (identifier) @dec (#eq? @dec "GrpcClient"))) @grpc_client_decorator
|
|
39
|
+
`,
|
|
40
|
+
};
|
|
41
|
+
// `.getService<X>('AuthService')` / `.getService('AuthService')`
|
|
42
|
+
const GET_SERVICE_SPEC = {
|
|
43
|
+
meta: {},
|
|
44
|
+
query: `
|
|
45
|
+
(call_expression
|
|
46
|
+
function: (member_expression
|
|
47
|
+
property: (property_identifier) @method (#eq? @method "getService"))
|
|
48
|
+
arguments: (arguments . [(string) (template_string)] @service))
|
|
49
|
+
`,
|
|
50
|
+
};
|
|
51
|
+
// `new XxxServiceClient(...)` — bare identifier constructor.
|
|
52
|
+
const NEW_SIMPLE_CTOR_SPEC = {
|
|
53
|
+
meta: {},
|
|
54
|
+
query: `
|
|
55
|
+
(new_expression
|
|
56
|
+
constructor: (identifier) @ctor)
|
|
57
|
+
`,
|
|
58
|
+
};
|
|
59
|
+
// `new foo.bar.XxxService(...)` — qualified constructor.
|
|
60
|
+
const NEW_QUALIFIED_CTOR_SPEC = {
|
|
61
|
+
meta: {},
|
|
62
|
+
query: `
|
|
63
|
+
(new_expression
|
|
64
|
+
constructor: (member_expression
|
|
65
|
+
property: (property_identifier) @ctor))
|
|
66
|
+
`,
|
|
67
|
+
};
|
|
68
|
+
// Detect whether the file uses `loadPackageDefinition` (gRPC dynamic
|
|
69
|
+
// proto loader). Matches either a bare call or an `obj.loadPackageDefinition(...)`
|
|
70
|
+
// call. Plugin gates the qualified-constructor consumer on this —
|
|
71
|
+
// structural check avoids materializing `tree.rootNode.text` for every file.
|
|
72
|
+
const LOAD_PACKAGE_DEFINITION_SPEC = {
|
|
73
|
+
meta: {},
|
|
74
|
+
query: `
|
|
75
|
+
(call_expression
|
|
76
|
+
function: [
|
|
77
|
+
(identifier) @fn (#eq? @fn "loadPackageDefinition")
|
|
78
|
+
(member_expression property: (property_identifier) @fn (#eq? @fn "loadPackageDefinition"))
|
|
79
|
+
])
|
|
80
|
+
`,
|
|
81
|
+
};
|
|
82
|
+
function compileBundle(language, name) {
|
|
83
|
+
const mk = (spec, suffix) => compilePatterns({
|
|
84
|
+
name: `${name}-${suffix}`,
|
|
85
|
+
language,
|
|
86
|
+
patterns: [spec],
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
grpcMethod: mk(GRPC_METHOD_SPEC, 'grpc-method'),
|
|
90
|
+
grpcClient: mk(GRPC_CLIENT_SPEC, 'grpc-client'),
|
|
91
|
+
getService: mk(GET_SERVICE_SPEC, 'get-service'),
|
|
92
|
+
newSimpleCtor: mk(NEW_SIMPLE_CTOR_SPEC, 'new-simple-ctor'),
|
|
93
|
+
newQualifiedCtor: mk(NEW_QUALIFIED_CTOR_SPEC, 'new-qualified-ctor'),
|
|
94
|
+
loadPackageDefinition: mk(LOAD_PACKAGE_DEFINITION_SPEC, 'load-package-definition'),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const JAVASCRIPT_BUNDLE = compileBundle(JavaScript, 'javascript-grpc');
|
|
98
|
+
const TYPESCRIPT_BUNDLE = compileBundle(TypeScript.typescript, 'typescript-grpc');
|
|
99
|
+
const TSX_BUNDLE = compileBundle(TypeScript.tsx, 'tsx-grpc');
|
|
100
|
+
/**
|
|
101
|
+
* Given a `@GrpcClient(...)` decorator node, find the type annotation
|
|
102
|
+
* text of the field it decorates (e.g. `AuthServiceClient`).
|
|
103
|
+
*
|
|
104
|
+
* In tree-sitter-typescript, decorators on class fields can appear in
|
|
105
|
+
* two configurations:
|
|
106
|
+
* - As a CHILD of `public_field_definition` alongside the field's
|
|
107
|
+
* type annotation (the common case for NestJS `@GrpcClient`).
|
|
108
|
+
* - As a SIBLING of the field in `class_body` (for method
|
|
109
|
+
* decorators, but kept for resilience against grammar variants).
|
|
110
|
+
* We walk the parent container and search for a type annotation.
|
|
111
|
+
*/
|
|
112
|
+
function resolveGrpcClientFieldType(decoratorNode) {
|
|
113
|
+
const parent = decoratorNode.parent;
|
|
114
|
+
if (!parent)
|
|
115
|
+
return null;
|
|
116
|
+
// Case 1: decorator is a child of the field definition — search
|
|
117
|
+
// the parent itself (which is the field definition) for a
|
|
118
|
+
// type_annotation child.
|
|
119
|
+
if (parent.type === 'public_field_definition' || parent.type.endsWith('field_definition')) {
|
|
120
|
+
return findFirstTypeAnnotationText(parent);
|
|
121
|
+
}
|
|
122
|
+
// Case 2: decorator is a sibling of the field in a class_body — walk
|
|
123
|
+
// forward through subsequent siblings until we find a node containing
|
|
124
|
+
// a type annotation.
|
|
125
|
+
for (let i = 0; i < parent.namedChildCount; i++) {
|
|
126
|
+
const child = parent.namedChild(i);
|
|
127
|
+
if (child && child.id === decoratorNode.id) {
|
|
128
|
+
for (let j = i + 1; j < parent.namedChildCount; j++) {
|
|
129
|
+
const next = parent.namedChild(j);
|
|
130
|
+
if (!next)
|
|
131
|
+
continue;
|
|
132
|
+
if (next.type === 'decorator')
|
|
133
|
+
continue;
|
|
134
|
+
const typeText = findFirstTypeAnnotationText(next);
|
|
135
|
+
if (typeText)
|
|
136
|
+
return typeText;
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Recursively search `node` for the first `type_annotation` child and
|
|
146
|
+
* return the text of its inner `type_identifier`, or null. Handles
|
|
147
|
+
* both `public_field_definition` and its variants.
|
|
148
|
+
*/
|
|
149
|
+
function findFirstTypeAnnotationText(node) {
|
|
150
|
+
if (node.type === 'type_annotation') {
|
|
151
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
152
|
+
const child = node.namedChild(i);
|
|
153
|
+
if (!child)
|
|
154
|
+
continue;
|
|
155
|
+
if (child.type === 'type_identifier')
|
|
156
|
+
return child.text;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
161
|
+
const child = node.namedChild(i);
|
|
162
|
+
if (!child)
|
|
163
|
+
continue;
|
|
164
|
+
const found = findFirstTypeAnnotationText(child);
|
|
165
|
+
if (found)
|
|
166
|
+
return found;
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
function scanBundle(bundle, tree) {
|
|
171
|
+
const out = [];
|
|
172
|
+
// ─── Provider: @GrpcMethod('Service', 'Method') ──────────────────
|
|
173
|
+
for (const match of runCompiledPatterns(bundle.grpcMethod, tree)) {
|
|
174
|
+
const svcNode = match.captures.service;
|
|
175
|
+
const methodNode = match.captures.method;
|
|
176
|
+
if (!svcNode || !methodNode)
|
|
177
|
+
continue;
|
|
178
|
+
const svc = unquoteLiteral(svcNode.text);
|
|
179
|
+
const mth = unquoteLiteral(methodNode.text);
|
|
180
|
+
if (!svc || !mth)
|
|
181
|
+
continue;
|
|
182
|
+
out.push({
|
|
183
|
+
role: 'provider',
|
|
184
|
+
serviceName: svc,
|
|
185
|
+
symbolName: `${svc}.${mth}`,
|
|
186
|
+
source: 'ts_grpc_method',
|
|
187
|
+
methodName: mth,
|
|
188
|
+
// @GrpcMethod hard-coded confidence 0.8 in the original code
|
|
189
|
+
// regardless of whether the proto map has a match.
|
|
190
|
+
confidenceWithProto: 0.8,
|
|
191
|
+
confidenceWithoutProto: 0.8,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// ─── Consumer: @GrpcClient() field with XxxServiceClient type ────
|
|
195
|
+
for (const match of runCompiledPatterns(bundle.grpcClient, tree)) {
|
|
196
|
+
const decoratorNode = match.captures.grpc_client_decorator;
|
|
197
|
+
if (!decoratorNode)
|
|
198
|
+
continue;
|
|
199
|
+
const typeText = resolveGrpcClientFieldType(decoratorNode);
|
|
200
|
+
if (!typeText)
|
|
201
|
+
continue;
|
|
202
|
+
const svcMatch = SERVICE_CLIENT_RE.exec(typeText);
|
|
203
|
+
if (!svcMatch)
|
|
204
|
+
continue;
|
|
205
|
+
const serviceName = svcMatch[1];
|
|
206
|
+
out.push({
|
|
207
|
+
role: 'consumer',
|
|
208
|
+
serviceName,
|
|
209
|
+
symbolName: `${serviceName}Client`,
|
|
210
|
+
source: 'ts_grpc_client_decorator',
|
|
211
|
+
confidenceWithProto: 0.75,
|
|
212
|
+
confidenceWithoutProto: 0.55,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
// ─── Consumer: client.getService<X>('Service') ───────────────────
|
|
216
|
+
for (const match of runCompiledPatterns(bundle.getService, tree)) {
|
|
217
|
+
const svcNode = match.captures.service;
|
|
218
|
+
if (!svcNode)
|
|
219
|
+
continue;
|
|
220
|
+
const svc = unquoteLiteral(svcNode.text);
|
|
221
|
+
if (!svc)
|
|
222
|
+
continue;
|
|
223
|
+
out.push({
|
|
224
|
+
role: 'consumer',
|
|
225
|
+
serviceName: svc,
|
|
226
|
+
symbolName: `${svc}Client`,
|
|
227
|
+
source: 'ts_client_grpc_get_service',
|
|
228
|
+
confidenceWithProto: 0.75,
|
|
229
|
+
confidenceWithoutProto: 0.55,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
// ─── Consumer: new XxxServiceClient(...) ─────────────────────────
|
|
233
|
+
for (const match of runCompiledPatterns(bundle.newSimpleCtor, tree)) {
|
|
234
|
+
const ctorNode = match.captures.ctor;
|
|
235
|
+
if (!ctorNode)
|
|
236
|
+
continue;
|
|
237
|
+
const svcMatch = SERVICE_CLIENT_RE.exec(ctorNode.text);
|
|
238
|
+
if (!svcMatch)
|
|
239
|
+
continue;
|
|
240
|
+
const serviceName = svcMatch[1];
|
|
241
|
+
out.push({
|
|
242
|
+
role: 'consumer',
|
|
243
|
+
serviceName,
|
|
244
|
+
symbolName: `${serviceName}Client`,
|
|
245
|
+
source: 'ts_generated_client',
|
|
246
|
+
confidenceWithProto: 0.75,
|
|
247
|
+
confidenceWithoutProto: 0.55,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
// ─── Consumer: loadPackageDefinition dynamic proto loader ────────
|
|
251
|
+
// Only emit when the file uses loadPackageDefinition, otherwise a
|
|
252
|
+
// generic `new foo.bar.Something()` in unrelated code would falsely
|
|
253
|
+
// register as a gRPC consumer. Check structurally via a dedicated
|
|
254
|
+
// query — avoids materializing `tree.rootNode.text` for the whole
|
|
255
|
+
// file (expensive on large files).
|
|
256
|
+
const usesLoadPackage = runCompiledPatterns(bundle.loadPackageDefinition, tree).length > 0;
|
|
257
|
+
if (usesLoadPackage) {
|
|
258
|
+
for (const match of runCompiledPatterns(bundle.newQualifiedCtor, tree)) {
|
|
259
|
+
const ctorNode = match.captures.ctor;
|
|
260
|
+
if (!ctorNode)
|
|
261
|
+
continue;
|
|
262
|
+
if (!CAPITALIZED_SERVICE_RE.test(ctorNode.text))
|
|
263
|
+
continue;
|
|
264
|
+
out.push({
|
|
265
|
+
role: 'consumer',
|
|
266
|
+
serviceName: ctorNode.text,
|
|
267
|
+
symbolName: `${ctorNode.text}Client`,
|
|
268
|
+
source: 'ts_load_package_definition',
|
|
269
|
+
confidenceWithProto: 0.75,
|
|
270
|
+
confidenceWithoutProto: 0.55,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
export const JAVASCRIPT_GRPC_PLUGIN = {
|
|
277
|
+
name: 'javascript-grpc',
|
|
278
|
+
language: JavaScript,
|
|
279
|
+
scan: (tree) => scanBundle(JAVASCRIPT_BUNDLE, tree),
|
|
280
|
+
};
|
|
281
|
+
export const TYPESCRIPT_GRPC_PLUGIN = {
|
|
282
|
+
name: 'typescript-grpc',
|
|
283
|
+
language: TypeScript.typescript,
|
|
284
|
+
scan: (tree) => scanBundle(TYPESCRIPT_BUNDLE, tree),
|
|
285
|
+
};
|
|
286
|
+
export const TSX_GRPC_PLUGIN = {
|
|
287
|
+
name: 'tsx-grpc',
|
|
288
|
+
language: TypeScript.tsx,
|
|
289
|
+
scan: (tree) => scanBundle(TSX_BUNDLE, tree),
|
|
290
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GrpcLanguagePlugin } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The proto plugin, or `null` if tree-sitter-proto is not available.
|
|
4
|
+
* The orchestrator checks this at import time and decides whether to
|
|
5
|
+
* use the tree-sitter path or the fallback manual parser.
|
|
6
|
+
*/
|
|
7
|
+
export declare const PROTO_GRPC_PLUGIN: GrpcLanguagePlugin | null;
|
|
8
|
+
/** The package declaration text from a proto file's tree. */
|
|
9
|
+
export declare function extractPackageFromTree(tree: import('tree-sitter').Tree): string;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { compilePatterns, runCompiledPatterns, } from '../tree-sitter-scanner.js';
|
|
3
|
+
/**
|
|
4
|
+
* Protobuf (.proto) tree-sitter plugin for gRPC contract extraction.
|
|
5
|
+
*
|
|
6
|
+
* Uses `tree-sitter-proto` (coder3101/tree-sitter-proto) as an
|
|
7
|
+
* optionalDependency — if the grammar is not installed (e.g. native
|
|
8
|
+
* compilation failed on an unusual platform), the plugin exports
|
|
9
|
+
* `null` and the orchestrator falls back to the existing manual
|
|
10
|
+
* string-sanitizing parser.
|
|
11
|
+
*
|
|
12
|
+
* The grammar is vendored in `vendor/tree-sitter-proto/` with
|
|
13
|
+
* parser.c regenerated against tree-sitter-cli 0.24 (ABI version 14)
|
|
14
|
+
* so it is compatible with the project's tree-sitter 0.25 runtime.
|
|
15
|
+
*/
|
|
16
|
+
const _require = createRequire(import.meta.url);
|
|
17
|
+
let ProtoGrammar = null;
|
|
18
|
+
try {
|
|
19
|
+
ProtoGrammar = _require('tree-sitter-proto');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Grammar not installed — PROTO_GRPC_PLUGIN will be null.
|
|
23
|
+
}
|
|
24
|
+
let PACKAGE_PATTERNS = null;
|
|
25
|
+
let SERVICE_PATTERNS = null;
|
|
26
|
+
if (ProtoGrammar) {
|
|
27
|
+
try {
|
|
28
|
+
// Validate that the grammar actually loads end-to-end: compile queries
|
|
29
|
+
// AND parse + walk a trivial proto file. tree-sitter's internal
|
|
30
|
+
// `initializeLanguageNodeClasses` can fail with a TDZ error in some
|
|
31
|
+
// test runners (vitest forks) when SyntaxNode isn't fully initialized
|
|
32
|
+
// yet. Catching that here ensures `PROTO_GRPC_PLUGIN` stays null and
|
|
33
|
+
// the orchestrator falls back to the manual parser.
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
const _Parser = _require('tree-sitter');
|
|
36
|
+
// Smoke-test: parse + setLanguage to verify the grammar is
|
|
37
|
+
// end-to-end compatible with this tree-sitter runtime.
|
|
38
|
+
const _testParser = new _Parser();
|
|
39
|
+
_testParser.setLanguage(ProtoGrammar);
|
|
40
|
+
_testParser.parse('service X { rpc Y (R) returns (R); }');
|
|
41
|
+
PACKAGE_PATTERNS = compilePatterns({
|
|
42
|
+
name: 'proto-package',
|
|
43
|
+
language: ProtoGrammar,
|
|
44
|
+
patterns: [
|
|
45
|
+
{
|
|
46
|
+
meta: {},
|
|
47
|
+
query: `(package (full_ident) @pkg)`,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
SERVICE_PATTERNS = compilePatterns({
|
|
52
|
+
name: 'proto-service',
|
|
53
|
+
language: ProtoGrammar,
|
|
54
|
+
patterns: [
|
|
55
|
+
{
|
|
56
|
+
meta: {},
|
|
57
|
+
query: `
|
|
58
|
+
(service
|
|
59
|
+
(service_name) @service_name
|
|
60
|
+
(rpc
|
|
61
|
+
(rpc_name) @rpc_name))
|
|
62
|
+
`,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Compilation failed (grammar ABI mismatch?) — fall back to null.
|
|
69
|
+
PACKAGE_PATTERNS = null;
|
|
70
|
+
SERVICE_PATTERNS = null;
|
|
71
|
+
ProtoGrammar = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function buildPlugin() {
|
|
75
|
+
if (!ProtoGrammar || !PACKAGE_PATTERNS || !SERVICE_PATTERNS)
|
|
76
|
+
return null;
|
|
77
|
+
const pkgPatterns = PACKAGE_PATTERNS;
|
|
78
|
+
const svcPatterns = SERVICE_PATTERNS;
|
|
79
|
+
return {
|
|
80
|
+
name: 'proto-grpc',
|
|
81
|
+
language: ProtoGrammar,
|
|
82
|
+
scan(tree) {
|
|
83
|
+
const out = [];
|
|
84
|
+
// Extract `package` declaration (first match wins).
|
|
85
|
+
let pkg = '';
|
|
86
|
+
for (const match of runCompiledPatterns(pkgPatterns, tree)) {
|
|
87
|
+
const pkgNode = match.captures.pkg;
|
|
88
|
+
if (pkgNode) {
|
|
89
|
+
pkg = pkgNode.text;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Extract `service → rpc` pairs. The query returns one match per
|
|
94
|
+
// (service, rpc) combination thanks to the nested structure.
|
|
95
|
+
for (const match of runCompiledPatterns(svcPatterns, tree)) {
|
|
96
|
+
const serviceNode = match.captures.service_name;
|
|
97
|
+
const rpcNode = match.captures.rpc_name;
|
|
98
|
+
if (!serviceNode || !rpcNode)
|
|
99
|
+
continue;
|
|
100
|
+
const serviceName = serviceNode.text;
|
|
101
|
+
const methodName = rpcNode.text;
|
|
102
|
+
out.push({
|
|
103
|
+
role: 'provider',
|
|
104
|
+
serviceName,
|
|
105
|
+
symbolName: `${serviceName}.${methodName}`,
|
|
106
|
+
source: 'proto',
|
|
107
|
+
methodName,
|
|
108
|
+
// Proto definitions are the canonical source of truth — always
|
|
109
|
+
// high confidence regardless of cross-referencing.
|
|
110
|
+
confidenceWithProto: 0.85,
|
|
111
|
+
confidenceWithoutProto: 0.85,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The proto plugin, or `null` if tree-sitter-proto is not available.
|
|
120
|
+
* The orchestrator checks this at import time and decides whether to
|
|
121
|
+
* use the tree-sitter path or the fallback manual parser.
|
|
122
|
+
*/
|
|
123
|
+
export const PROTO_GRPC_PLUGIN = buildPlugin();
|
|
124
|
+
/** The package declaration text from a proto file's tree. */
|
|
125
|
+
export function extractPackageFromTree(tree) {
|
|
126
|
+
if (!PACKAGE_PATTERNS)
|
|
127
|
+
return '';
|
|
128
|
+
for (const match of runCompiledPatterns(PACKAGE_PATTERNS, tree)) {
|
|
129
|
+
const pkgNode = match.captures.pkg;
|
|
130
|
+
if (pkgNode)
|
|
131
|
+
return pkgNode.text;
|
|
132
|
+
}
|
|
133
|
+
return '';
|
|
134
|
+
}
|