gitnexus 1.6.12-rc.26 → 1.6.12-rc.27
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/core/ingestion/languages/ruby/import-target.d.ts +9 -7
- package/dist/core/ingestion/languages/ruby/import-target.js +69 -8
- package/dist/core/ingestion/languages/ruby/resolution-config.d.ts +28 -0
- package/dist/core/ingestion/languages/ruby/resolution-config.js +311 -0
- package/dist/core/ingestion/languages/ruby/scope-resolver.js +2 -0
- package/dist/core/lbug/schema.d.ts +4 -2
- package/dist/core/lbug/schema.js +4 -1
- package/package.json +1 -1
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Ruby import resolution rules:
|
|
5
5
|
* - `require_relative './foo'` → resolve relative to the importing file's dir
|
|
6
|
-
* - `require 'foo'`
|
|
7
|
-
* -
|
|
6
|
+
* - `require 'foo'` → use scoped gem metadata before legacy suffix matching
|
|
7
|
+
* - Known local gems → resolve only within their declared load roots
|
|
8
|
+
* - Known external gems → null, even if an unrelated repo file suffix matches
|
|
8
9
|
*/
|
|
9
10
|
export interface RubyResolveContext {
|
|
10
11
|
readonly fromFile: string;
|
|
@@ -18,9 +19,10 @@ export interface RubyResolveContext {
|
|
|
18
19
|
* against the importing file's directory, trying `.rb` and `/index.rb`
|
|
19
20
|
* suffixes.
|
|
20
21
|
*
|
|
21
|
-
* For bare requires
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* For bare requires, scoped manifest metadata takes precedence: known local
|
|
23
|
+
* gems resolve only within their declared load roots (a miss returns `null`),
|
|
24
|
+
* and known external gem prefixes return `null` even if a repo suffix matches.
|
|
25
|
+
* Only requires without matching gem evidence delegate to the existing
|
|
26
|
+
* `resolveRubyImportInternal` suffix matcher.
|
|
25
27
|
*/
|
|
26
|
-
export declare function resolveRubyImportTarget(targetRaw: string, fromFile: string, allFilePaths: ReadonlySet<string>,
|
|
28
|
+
export declare function resolveRubyImportTarget(targetRaw: string, fromFile: string, allFilePaths: ReadonlySet<string>, resolutionConfig?: unknown): string | readonly string[] | null;
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Ruby import resolution rules:
|
|
5
5
|
* - `require_relative './foo'` → resolve relative to the importing file's dir
|
|
6
|
-
* - `require 'foo'`
|
|
7
|
-
* -
|
|
6
|
+
* - `require 'foo'` → use scoped gem metadata before legacy suffix matching
|
|
7
|
+
* - Known local gems → resolve only within their declared load roots
|
|
8
|
+
* - Known external gems → null, even if an unrelated repo file suffix matches
|
|
8
9
|
*/
|
|
9
10
|
import { resolveRubyImportInternal } from '../../import-resolvers/ruby.js';
|
|
10
11
|
import { getWorkspaceFileIndex } from '../../import-resolvers/workspace-file-index.js';
|
|
11
12
|
import { isHeritageMarker } from '../../utils/heritage-marker.js';
|
|
13
|
+
import { findRubyResolutionScope, } from './resolution-config.js';
|
|
12
14
|
// ─── resolveRubyImportTarget ──────────────────────────────────────────────
|
|
13
15
|
/**
|
|
14
16
|
* ScopeResolver-shaped adapter:
|
|
@@ -18,12 +20,13 @@ import { isHeritageMarker } from '../../utils/heritage-marker.js';
|
|
|
18
20
|
* against the importing file's directory, trying `.rb` and `/index.rb`
|
|
19
21
|
* suffixes.
|
|
20
22
|
*
|
|
21
|
-
* For bare requires
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* For bare requires, scoped manifest metadata takes precedence: known local
|
|
24
|
+
* gems resolve only within their declared load roots (a miss returns `null`),
|
|
25
|
+
* and known external gem prefixes return `null` even if a repo suffix matches.
|
|
26
|
+
* Only requires without matching gem evidence delegate to the existing
|
|
27
|
+
* `resolveRubyImportInternal` suffix matcher.
|
|
25
28
|
*/
|
|
26
|
-
export function resolveRubyImportTarget(targetRaw, fromFile, allFilePaths,
|
|
29
|
+
export function resolveRubyImportTarget(targetRaw, fromFile, allFilePaths, resolutionConfig) {
|
|
27
30
|
if (!targetRaw)
|
|
28
31
|
return null;
|
|
29
32
|
if (isHeritageMarker(targetRaw))
|
|
@@ -37,7 +40,16 @@ export function resolveRubyImportTarget(targetRaw, fromFile, allFilePaths, _reso
|
|
|
37
40
|
const resolved = resolveRelative(targetRaw, fromDir, allFilePaths);
|
|
38
41
|
return resolved;
|
|
39
42
|
}
|
|
40
|
-
// ── require:
|
|
43
|
+
// ── require: scoped gem evidence before repository-wide fallback ────
|
|
44
|
+
const config = resolutionConfig;
|
|
45
|
+
const scope = config === null || config === undefined ? undefined : findRubyResolutionScope(config, fromFile);
|
|
46
|
+
if (scope !== undefined) {
|
|
47
|
+
const localGemTarget = resolveLocalGemTarget(targetRaw, scope, allFilePaths);
|
|
48
|
+
if (localGemTarget !== undefined)
|
|
49
|
+
return localGemTarget;
|
|
50
|
+
if (matchesRequirePrefix(targetRaw, scope.externalRequirePrefixes))
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
41
53
|
return resolveBare(targetRaw, allFilePaths);
|
|
42
54
|
}
|
|
43
55
|
// ─── internal helpers ─────────────────────────────────────────────────────
|
|
@@ -73,6 +85,55 @@ function resolveRelative(targetRaw, fromDir, allFilePaths) {
|
|
|
73
85
|
return resolvedPath;
|
|
74
86
|
return null;
|
|
75
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Yield the require stem, then each slash-delimited ancestor. This makes both
|
|
90
|
+
* local-root and external-gem lookup O(require path depth), not O(gem count).
|
|
91
|
+
* A trailing `.rb` is stripped first so `require 'my_engine.rb'` matches the
|
|
92
|
+
* configured prefix `my_engine`, matching Ruby's optional-suffix require.
|
|
93
|
+
*/
|
|
94
|
+
function requirePrefixCandidates(targetRaw) {
|
|
95
|
+
const stem = targetRaw.endsWith('.rb') ? targetRaw.slice(0, -3) : targetRaw;
|
|
96
|
+
if (stem.length === 0)
|
|
97
|
+
return [];
|
|
98
|
+
const candidates = [stem];
|
|
99
|
+
let slash = stem.lastIndexOf('/');
|
|
100
|
+
while (slash !== -1) {
|
|
101
|
+
candidates.push(stem.slice(0, slash));
|
|
102
|
+
slash = stem.lastIndexOf('/', slash - 1);
|
|
103
|
+
}
|
|
104
|
+
return candidates;
|
|
105
|
+
}
|
|
106
|
+
function matchesRequirePrefix(targetRaw, prefixes) {
|
|
107
|
+
return requirePrefixCandidates(targetRaw).some((candidate) => prefixes.has(candidate));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Resolve a path/gemspec-backed gem against its declared Ruby load roots.
|
|
111
|
+
* `undefined` means no local-gem prefix matched; `null` means one matched but
|
|
112
|
+
* none of its declared load roots contained the target.
|
|
113
|
+
*/
|
|
114
|
+
function resolveLocalGemTarget(targetRaw, scope, allFilePaths) {
|
|
115
|
+
for (const prefix of requirePrefixCandidates(targetRaw)) {
|
|
116
|
+
const loadRoots = scope.localLoadRootsByPrefix.get(prefix);
|
|
117
|
+
if (loadRoots === undefined)
|
|
118
|
+
continue;
|
|
119
|
+
for (const loadRoot of loadRoots) {
|
|
120
|
+
const base = loadRoot ? `${loadRoot}/${targetRaw}` : targetRaw;
|
|
121
|
+
if (base.endsWith('.rb')) {
|
|
122
|
+
if (allFilePaths.has(base))
|
|
123
|
+
return base;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const rbFile = `${base}.rb`;
|
|
127
|
+
if (allFilePaths.has(rbFile))
|
|
128
|
+
return rbFile;
|
|
129
|
+
}
|
|
130
|
+
// The prefix is owned by a known local gem. If its declared roots do not
|
|
131
|
+
// contain the target, repository-wide suffix matching would fabricate an
|
|
132
|
+
// edge to an unrelated file.
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
76
137
|
/**
|
|
77
138
|
* Resolve a bare require path (`'serializable'`, `'json'`, `'net/http'`)
|
|
78
139
|
* via suffix matching using the existing Ruby import resolver.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface RubyResolutionScope {
|
|
2
|
+
/** Require prefixes provided by gems whose source is outside this repository. */
|
|
3
|
+
readonly externalRequirePrefixes: ReadonlySet<string>;
|
|
4
|
+
/** Require prefix -> repository-relative Ruby load roots for local gems. */
|
|
5
|
+
readonly localLoadRootsByPrefix: ReadonlyMap<string, readonly string[]>;
|
|
6
|
+
}
|
|
7
|
+
export interface RubyResolutionConfig {
|
|
8
|
+
/** Manifest directory (repository-relative POSIX path) -> dependency scope. */
|
|
9
|
+
readonly scopesByDirectory: ReadonlyMap<string, RubyResolutionScope>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Select the nearest manifest directory that owns `fromFile`.
|
|
13
|
+
*
|
|
14
|
+
* Walking ancestors makes lookup O(path depth), independent of how many
|
|
15
|
+
* sibling Gemfiles a monorepo contains.
|
|
16
|
+
*/
|
|
17
|
+
export declare function findRubyResolutionScope(config: RubyResolutionConfig, fromFile: string): RubyResolutionScope | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Statically collect Ruby dependency sources without evaluating Gemfile or
|
|
20
|
+
* gemspec code. Scopes stay separate by manifest directory so sibling projects
|
|
21
|
+
* in a monorepo cannot suppress one another's local imports. Lockfiles
|
|
22
|
+
* contribute remote GEM/GIT specs and local PATH/GEMSPEC load roots only when
|
|
23
|
+
* an adjacent Gemfile or gemspec establishes a Bundler/RubyGems project.
|
|
24
|
+
*
|
|
25
|
+
* No declarative manifest means no safe gate, so return null and preserve the
|
|
26
|
+
* resolver's existing fail-open behavior for loose script directories.
|
|
27
|
+
*/
|
|
28
|
+
export declare function loadRubyResolutionConfig(repoPath: string): RubyResolutionConfig | null;
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
const MAX_VISITED_DIRECTORIES = 20_000;
|
|
4
|
+
const SKIP_DIRECTORIES = new Set([
|
|
5
|
+
'.git',
|
|
6
|
+
'.gitnexus',
|
|
7
|
+
'node_modules',
|
|
8
|
+
'vendor',
|
|
9
|
+
'dist',
|
|
10
|
+
'build',
|
|
11
|
+
'coverage',
|
|
12
|
+
]);
|
|
13
|
+
const GEMFILE_DEPENDENCY = /^\s*gem\s*(?:\(\s*)?(['"])([A-Za-z0-9_.-]+)\1(?<options>.*)$/;
|
|
14
|
+
const GEMSPEC_DEPENDENCY = /^\s*(?:[A-Za-z_]\w*\.)?(?:add_dependency|add_runtime_dependency|add_development_dependency)\s*(?:\(\s*)?(['"])([A-Za-z0-9_.-]+)\1/;
|
|
15
|
+
const GEMSPEC_NAME = /^\s*(?:[A-Za-z_]\w*\.)?name\s*=\s*(['"])([A-Za-z0-9_.-]+)\1/;
|
|
16
|
+
const GEMSPEC_REQUIRE_PATHS = /^\s*(?:[A-Za-z_]\w*\.)?require_paths\s*=\s*\[([^\]]*)\]/;
|
|
17
|
+
const QUOTED_LITERAL = /(['"])([^'"]+)\1/g;
|
|
18
|
+
const LOCKFILE_SECTION = /^([A-Z][A-Z ]*)\s*$/;
|
|
19
|
+
const LOCKFILE_REMOTE = /^ {2}remote:\s*(.+?)\s*$/;
|
|
20
|
+
const LOCKFILE_SPECS_HEADER = /^ {2}specs:\s*$/;
|
|
21
|
+
const LOCKFILE_SPEC = /^ {4}([A-Za-z0-9_.-]+) \(/;
|
|
22
|
+
function createMutableScope() {
|
|
23
|
+
return {
|
|
24
|
+
externalRequirePrefixes: new Set(),
|
|
25
|
+
localLoadRootsByPrefix: new Map(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function normalizeRepoPath(filePath) {
|
|
29
|
+
return filePath.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '');
|
|
30
|
+
}
|
|
31
|
+
function repoRelativePath(repoPath, candidate) {
|
|
32
|
+
const rel = relative(resolve(repoPath), resolve(candidate));
|
|
33
|
+
if (rel === '')
|
|
34
|
+
return '';
|
|
35
|
+
if (rel === '..' || rel.startsWith(`..\\`) || rel.startsWith('../') || isAbsolute(rel)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return normalizeRepoPath(rel);
|
|
39
|
+
}
|
|
40
|
+
function requirePrefixes(gemName, requireAs) {
|
|
41
|
+
const prefixes = new Set([gemName]);
|
|
42
|
+
if (gemName.includes('-'))
|
|
43
|
+
prefixes.add(gemName.replaceAll('-', '/'));
|
|
44
|
+
if (requireAs !== undefined && requireAs.length > 0)
|
|
45
|
+
prefixes.add(requireAs);
|
|
46
|
+
return [...prefixes];
|
|
47
|
+
}
|
|
48
|
+
function literalOption(options, name) {
|
|
49
|
+
const modern = new RegExp(`(?:^|[,\\s])${name}\\s*:\\s*(['"])([^'"]+)\\1`).exec(options);
|
|
50
|
+
if (modern !== null)
|
|
51
|
+
return modern[2];
|
|
52
|
+
const hashRocket = new RegExp(`(?:^|[,\\s]):${name}\\s*=>\\s*(['"])([^'"]+)\\1`).exec(options);
|
|
53
|
+
return hashRocket?.[2];
|
|
54
|
+
}
|
|
55
|
+
function hasOption(options, name) {
|
|
56
|
+
return (new RegExp(`(?:^|[,\\s])${name}\\s*:`).test(options) ||
|
|
57
|
+
new RegExp(`(?:^|[,\\s]):${name}\\s*=>`).test(options));
|
|
58
|
+
}
|
|
59
|
+
function addExternalGem(scope, gemName, requireAs) {
|
|
60
|
+
for (const prefix of requirePrefixes(gemName, requireAs)) {
|
|
61
|
+
if (!scope.localLoadRootsByPrefix.has(prefix))
|
|
62
|
+
scope.externalRequirePrefixes.add(prefix);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function addLocalGem(scope, gemName, loadRoots, requireAs) {
|
|
66
|
+
for (const prefix of requirePrefixes(gemName, requireAs)) {
|
|
67
|
+
scope.externalRequirePrefixes.delete(prefix);
|
|
68
|
+
let roots = scope.localLoadRootsByPrefix.get(prefix);
|
|
69
|
+
if (roots === undefined) {
|
|
70
|
+
roots = new Set();
|
|
71
|
+
scope.localLoadRootsByPrefix.set(prefix, roots);
|
|
72
|
+
}
|
|
73
|
+
for (const loadRoot of loadRoots)
|
|
74
|
+
roots.add(loadRoot);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function parseGemspecMetadata(contents) {
|
|
78
|
+
let name = null;
|
|
79
|
+
let requirePaths = null;
|
|
80
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
81
|
+
const nameMatch = GEMSPEC_NAME.exec(line);
|
|
82
|
+
if (nameMatch !== null)
|
|
83
|
+
name = nameMatch[2];
|
|
84
|
+
const requirePathsMatch = GEMSPEC_REQUIRE_PATHS.exec(line);
|
|
85
|
+
if (requirePathsMatch === null)
|
|
86
|
+
continue;
|
|
87
|
+
const paths = [];
|
|
88
|
+
for (const match of requirePathsMatch[1].matchAll(QUOTED_LITERAL))
|
|
89
|
+
paths.push(match[2]);
|
|
90
|
+
if (paths.length > 0)
|
|
91
|
+
requirePaths = paths;
|
|
92
|
+
}
|
|
93
|
+
return { name, requirePaths: requirePaths ?? ['lib'] };
|
|
94
|
+
}
|
|
95
|
+
function loadLocalGemRoots(repoPath, gemRoot, expectedGemName) {
|
|
96
|
+
const gemRootRelative = repoRelativePath(repoPath, gemRoot);
|
|
97
|
+
if (gemRootRelative === null)
|
|
98
|
+
return null;
|
|
99
|
+
let requirePaths = ['lib'];
|
|
100
|
+
try {
|
|
101
|
+
const gemspecs = readdirSync(gemRoot, { withFileTypes: true })
|
|
102
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.gemspec'))
|
|
103
|
+
.map((entry) => join(gemRoot, entry.name))
|
|
104
|
+
.sort();
|
|
105
|
+
for (const gemspec of gemspecs) {
|
|
106
|
+
const metadata = parseGemspecMetadata(readFileSync(gemspec, 'utf8'));
|
|
107
|
+
if (metadata.name === expectedGemName) {
|
|
108
|
+
requirePaths = metadata.requirePaths;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// An unreadable local gem stays local; the conventional lib root is a
|
|
115
|
+
// bounded best effort, and resolution falls through if no file matches.
|
|
116
|
+
}
|
|
117
|
+
return requirePaths
|
|
118
|
+
.map((requirePath) => repoRelativePath(repoPath, resolve(gemRoot, requirePath)))
|
|
119
|
+
.filter((loadRoot) => loadRoot !== null);
|
|
120
|
+
}
|
|
121
|
+
function addGemfileDependencies(contents, manifestDirectory, repoPath, scope) {
|
|
122
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
123
|
+
const match = GEMFILE_DEPENDENCY.exec(line);
|
|
124
|
+
if (match === null)
|
|
125
|
+
continue;
|
|
126
|
+
const gemName = match[2];
|
|
127
|
+
const options = match.groups?.options ?? '';
|
|
128
|
+
const requireAs = literalOption(options, 'require');
|
|
129
|
+
const localPath = literalOption(options, 'path');
|
|
130
|
+
if (localPath !== undefined) {
|
|
131
|
+
const loadRoots = loadLocalGemRoots(repoPath, resolve(manifestDirectory, localPath), gemName);
|
|
132
|
+
if (loadRoots === null)
|
|
133
|
+
addExternalGem(scope, gemName, requireAs);
|
|
134
|
+
else
|
|
135
|
+
addLocalGem(scope, gemName, loadRoots, requireAs);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// A dynamic path expression cannot be classified without evaluating Ruby.
|
|
139
|
+
// Fail open instead of deleting a potentially real in-repo import edge.
|
|
140
|
+
if (hasOption(options, 'path'))
|
|
141
|
+
continue;
|
|
142
|
+
addExternalGem(scope, gemName, requireAs);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function addGemspecDependencies(contents, gemspecDirectory, repoPath, scope) {
|
|
146
|
+
const metadata = parseGemspecMetadata(contents);
|
|
147
|
+
if (metadata.name !== null) {
|
|
148
|
+
if (repoRelativePath(repoPath, gemspecDirectory) !== null) {
|
|
149
|
+
const loadRoots = metadata.requirePaths
|
|
150
|
+
.map((requirePath) => repoRelativePath(repoPath, resolve(gemspecDirectory, requirePath)))
|
|
151
|
+
.filter((loadRoot) => loadRoot !== null);
|
|
152
|
+
addLocalGem(scope, metadata.name, loadRoots);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
156
|
+
const match = GEMSPEC_DEPENDENCY.exec(line);
|
|
157
|
+
if (match !== null)
|
|
158
|
+
addExternalGem(scope, match[2]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function addLockedDependencies(contents, lockfileDirectory, repoPath, scope) {
|
|
162
|
+
let section = '';
|
|
163
|
+
let remote = null;
|
|
164
|
+
let inSpecs = false;
|
|
165
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
166
|
+
const sectionMatch = LOCKFILE_SECTION.exec(line);
|
|
167
|
+
if (sectionMatch !== null) {
|
|
168
|
+
section = sectionMatch[1];
|
|
169
|
+
remote = null;
|
|
170
|
+
inSpecs = false;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const remoteMatch = LOCKFILE_REMOTE.exec(line);
|
|
174
|
+
if (remoteMatch !== null) {
|
|
175
|
+
remote = remoteMatch[1];
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (LOCKFILE_SPECS_HEADER.test(line)) {
|
|
179
|
+
inSpecs = true;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (!inSpecs)
|
|
183
|
+
continue;
|
|
184
|
+
const specMatch = LOCKFILE_SPEC.exec(line);
|
|
185
|
+
if (specMatch === null)
|
|
186
|
+
continue;
|
|
187
|
+
const gemName = specMatch[1];
|
|
188
|
+
if (section === 'GEM' || section === 'GIT') {
|
|
189
|
+
addExternalGem(scope, gemName);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (section !== 'PATH' && section !== 'GEMSPEC')
|
|
193
|
+
continue;
|
|
194
|
+
if (remote === null)
|
|
195
|
+
continue;
|
|
196
|
+
const loadRoots = loadLocalGemRoots(repoPath, resolve(lockfileDirectory, remote), gemName);
|
|
197
|
+
if (loadRoots === null)
|
|
198
|
+
addExternalGem(scope, gemName);
|
|
199
|
+
else
|
|
200
|
+
addLocalGem(scope, gemName, loadRoots);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function freezeScope(scope) {
|
|
204
|
+
const localLoadRootsByPrefix = new Map();
|
|
205
|
+
for (const [prefix, roots] of scope.localLoadRootsByPrefix) {
|
|
206
|
+
localLoadRootsByPrefix.set(prefix, [...roots].sort());
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
externalRequirePrefixes: new Set(scope.externalRequirePrefixes),
|
|
210
|
+
localLoadRootsByPrefix,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Select the nearest manifest directory that owns `fromFile`.
|
|
215
|
+
*
|
|
216
|
+
* Walking ancestors makes lookup O(path depth), independent of how many
|
|
217
|
+
* sibling Gemfiles a monorepo contains.
|
|
218
|
+
*/
|
|
219
|
+
export function findRubyResolutionScope(config, fromFile) {
|
|
220
|
+
const normalized = normalizeRepoPath(fromFile);
|
|
221
|
+
let directory = normalized.includes('/') ? normalized.slice(0, normalized.lastIndexOf('/')) : '';
|
|
222
|
+
for (;;) {
|
|
223
|
+
const scope = config.scopesByDirectory.get(directory);
|
|
224
|
+
if (scope !== undefined)
|
|
225
|
+
return scope;
|
|
226
|
+
if (directory === '')
|
|
227
|
+
return undefined;
|
|
228
|
+
const slash = directory.lastIndexOf('/');
|
|
229
|
+
directory = slash === -1 ? '' : directory.slice(0, slash);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Statically collect Ruby dependency sources without evaluating Gemfile or
|
|
234
|
+
* gemspec code. Scopes stay separate by manifest directory so sibling projects
|
|
235
|
+
* in a monorepo cannot suppress one another's local imports. Lockfiles
|
|
236
|
+
* contribute remote GEM/GIT specs and local PATH/GEMSPEC load roots only when
|
|
237
|
+
* an adjacent Gemfile or gemspec establishes a Bundler/RubyGems project.
|
|
238
|
+
*
|
|
239
|
+
* No declarative manifest means no safe gate, so return null and preserve the
|
|
240
|
+
* resolver's existing fail-open behavior for loose script directories.
|
|
241
|
+
*/
|
|
242
|
+
export function loadRubyResolutionConfig(repoPath) {
|
|
243
|
+
const pending = [repoPath];
|
|
244
|
+
const manifestsByDirectory = new Map();
|
|
245
|
+
const lockfilesByDirectory = new Map();
|
|
246
|
+
let visitedDirectories = 0;
|
|
247
|
+
while (pending.length > 0 && visitedDirectories < MAX_VISITED_DIRECTORIES) {
|
|
248
|
+
const directory = pending.pop();
|
|
249
|
+
if (directory === undefined)
|
|
250
|
+
break;
|
|
251
|
+
visitedDirectories++;
|
|
252
|
+
let entries;
|
|
253
|
+
try {
|
|
254
|
+
entries = readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
for (const entry of entries) {
|
|
260
|
+
if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) {
|
|
261
|
+
pending.push(join(directory, entry.name));
|
|
262
|
+
}
|
|
263
|
+
else if (entry.isFile() && (entry.name === 'Gemfile' || entry.name.endsWith('.gemspec'))) {
|
|
264
|
+
const manifests = manifestsByDirectory.get(directory) ?? [];
|
|
265
|
+
manifests.push(join(directory, entry.name));
|
|
266
|
+
manifestsByDirectory.set(directory, manifests);
|
|
267
|
+
}
|
|
268
|
+
else if (entry.isFile() && entry.name === 'Gemfile.lock') {
|
|
269
|
+
lockfilesByDirectory.set(directory, join(directory, entry.name));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (manifestsByDirectory.size === 0)
|
|
274
|
+
return null;
|
|
275
|
+
const mutableScopes = new Map();
|
|
276
|
+
for (const [directory, manifests] of [...manifestsByDirectory].sort(([left], [right]) => left.localeCompare(right))) {
|
|
277
|
+
const directoryRelative = repoRelativePath(repoPath, directory);
|
|
278
|
+
if (directoryRelative === null)
|
|
279
|
+
continue;
|
|
280
|
+
const scope = createMutableScope();
|
|
281
|
+
mutableScopes.set(directoryRelative, scope);
|
|
282
|
+
for (const manifest of manifests.sort()) {
|
|
283
|
+
try {
|
|
284
|
+
const contents = readFileSync(manifest, 'utf8');
|
|
285
|
+
if (manifest.endsWith('.gemspec')) {
|
|
286
|
+
addGemspecDependencies(contents, directory, repoPath, scope);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
addGemfileDependencies(contents, directory, repoPath, scope);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// A partially readable scope remains fail-open for unknown gems.
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const lockfile = lockfilesByDirectory.get(directory);
|
|
297
|
+
if (lockfile === undefined)
|
|
298
|
+
continue;
|
|
299
|
+
try {
|
|
300
|
+
addLockedDependencies(readFileSync(lockfile, 'utf8'), directory, repoPath, scope);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// Direct declarations still provide bounded evidence when the lock is unreadable.
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const scopesByDirectory = new Map();
|
|
307
|
+
for (const [directory, scope] of mutableScopes) {
|
|
308
|
+
scopesByDirectory.set(directory, freezeScope(scope));
|
|
309
|
+
}
|
|
310
|
+
return { scopesByDirectory };
|
|
311
|
+
}
|
|
@@ -7,6 +7,7 @@ import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
|
|
|
7
7
|
import { generateId } from '../../../../lib/utils.js';
|
|
8
8
|
import { decodeMarker } from '../../utils/heritage-marker.js';
|
|
9
9
|
import { rubyIsGlobalNameFallbackPlausible } from './name-fallback-visibility.js';
|
|
10
|
+
import { loadRubyResolutionConfig } from './resolution-config.js';
|
|
10
11
|
/**
|
|
11
12
|
* #1991: resolve a BARE mixin reference (`include Loggable`) to a nested module by
|
|
12
13
|
* the INCLUDING class's lexical scope — Ruby looks up a constant in the innermost
|
|
@@ -248,6 +249,7 @@ export const rubyScopeResolver = {
|
|
|
248
249
|
languageProvider: rubyProvider,
|
|
249
250
|
importEdgeReason: 'ruby-scope: import',
|
|
250
251
|
resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig) => resolveRubyImportTarget(targetRaw, fromFile, allFilePaths, resolutionConfig),
|
|
252
|
+
loadResolutionConfig: (repoPath) => loadRubyResolutionConfig(repoPath),
|
|
251
253
|
expandsWildcardTo: (targetModuleScope, parsedFiles) => expandRubyWildcardNames(targetModuleScope, parsedFiles),
|
|
252
254
|
mergeBindings: (existing, incoming, scopeId) => rubyMergeBindings(existing, incoming, scopeId),
|
|
253
255
|
arityCompatibility: (callsite, def) => rubyArityCompatibility(def, callsite),
|
|
@@ -47,7 +47,7 @@ export declare const DESTINATION_SCHEMA = "\nCREATE NODE TABLE Destination (\n
|
|
|
47
47
|
export declare const SECTION_SCHEMA = "\nCREATE NODE TABLE Section (\n id STRING,\n name STRING,\n filePath STRING,\n startLine INT64,\n endLine INT64,\n level INT64,\n content STRING,\n description STRING,\n PRIMARY KEY (id)\n)";
|
|
48
48
|
export declare const BASICBLOCK_SCHEMA = "\nCREATE NODE TABLE BasicBlock (\n id STRING,\n filePath STRING,\n startLine INT64,\n endLine INT64,\n text STRING,\n callees STRING,\n calleeIds STRING,\n PRIMARY KEY (id)\n)";
|
|
49
49
|
/**
|
|
50
|
-
* The
|
|
50
|
+
* The pairs NEITHER rule above generates — everything left after the two
|
|
51
51
|
* cross products are subtracted. Carried by CONTAINMENT, inheritance, imports
|
|
52
52
|
* and DI: a container label crossed with a contained label. No predicate
|
|
53
53
|
* describes that surface (any container can hold any definition).
|
|
@@ -60,6 +60,8 @@ export declare const BASICBLOCK_SCHEMA = "\nCREATE NODE TABLE BasicBlock (\n id
|
|
|
60
60
|
* templates), the two `Route|Process` / `Tool|Process` entry points whose
|
|
61
61
|
* emitter names both labels as literals, and `BasicBlock|BasicBlock`, the PDG
|
|
62
62
|
* substrate.
|
|
63
|
+
* Spring dynamic lookup in a constructor also emits INJECTS to a synthetic
|
|
64
|
+
* @Bean CodeElement (#3238), so Constructor|CodeElement belongs here.
|
|
63
65
|
*
|
|
64
66
|
* NOTHING A RULE ALREADY COVERS BELONGS HERE. `generatedRelationPairs` skips
|
|
65
67
|
* any pair present in this block, so a redundant line does not merely duplicate
|
|
@@ -96,7 +98,7 @@ export declare const BASICBLOCK_SCHEMA = "\nCREATE NODE TABLE BasicBlock (\n id
|
|
|
96
98
|
* `test/integration/structural-pair-coverage.test.ts` guards this half from a
|
|
97
99
|
* corpus — that is the guard, not this comment.
|
|
98
100
|
*/
|
|
99
|
-
export declare const STRUCTURAL_PAIR_DDL = " FROM File TO Folder,\n FROM File TO CodeElement,\n FROM File TO `Typedef`,\n FROM File TO `Namespace`,\n FROM File TO `Impl`,\n FROM File TO `Static`,\n FROM File TO `Template`,\n FROM File TO Section,\n FROM Folder TO Folder,\n FROM Folder TO File,\n FROM Function TO `Template`,\n FROM Function TO `Namespace`,\n FROM Function TO `Impl`,\n FROM Function TO `Typedef`,\n FROM Function TO CodeElement,\n FROM Class TO `Template`,\n FROM Class TO `Impl`,\n FROM Class TO `Namespace`,\n FROM Class TO `Typedef`,\n FROM Class TO CodeElement,\n FROM Method TO `Template`,\n FROM Method TO `Namespace`,\n FROM Method TO `Impl`,\n FROM Method TO CodeElement,\n FROM `Template` TO `Template`,\n FROM `Template` TO Function,\n FROM `Template` TO Method,\n FROM `Template` TO Class,\n FROM `Template` TO `Struct`,\n FROM `Template` TO `TypeAlias`,\n FROM `Template` TO `Enum`,\n FROM `Template` TO `Macro`,\n FROM `Template` TO Interface,\n FROM `Template` TO `Constructor`,\n FROM `Module` TO CodeElement,\n FROM `Module` TO `Namespace`,\n FROM `Namespace` TO Function,\n FROM CodeElement TO CodeElement,\n FROM CodeElement TO Class,\n FROM CodeElement TO Category,\n FROM CodeElement TO Method,\n FROM CodeElement TO `Module`,\n FROM CodeElement TO `Property`,\n FROM Section TO Section,\n FROM Interface TO CodeElement,\n FROM `Namespace` TO `Struct`,\n FROM `Impl` TO Method,\n FROM `Impl` TO Function,\n FROM `Impl` TO `Constructor`,\n FROM `Impl` TO `Property`,\n FROM `Impl` TO `Trait`,\n FROM `Impl` TO `Struct`,\n FROM `Impl` TO `Impl`,\n FROM `Constructor` TO `Template`,\n FROM `Constructor` TO `Impl`,\n FROM `Constructor` TO `Namespace`,\n FROM `Constructor` TO `Typedef`,\n FROM Route TO Process,\n FROM Tool TO Process,\n FROM Destination TO `Property`,\n FROM BasicBlock TO BasicBlock";
|
|
101
|
+
export declare const STRUCTURAL_PAIR_DDL = " FROM File TO Folder,\n FROM File TO CodeElement,\n FROM File TO `Typedef`,\n FROM File TO `Namespace`,\n FROM File TO `Impl`,\n FROM File TO `Static`,\n FROM File TO `Template`,\n FROM File TO Section,\n FROM Folder TO Folder,\n FROM Folder TO File,\n FROM Function TO `Template`,\n FROM Function TO `Namespace`,\n FROM Function TO `Impl`,\n FROM Function TO `Typedef`,\n FROM Function TO CodeElement,\n FROM Class TO `Template`,\n FROM Class TO `Impl`,\n FROM Class TO `Namespace`,\n FROM Class TO `Typedef`,\n FROM Class TO CodeElement,\n FROM Method TO `Template`,\n FROM Method TO `Namespace`,\n FROM Method TO `Impl`,\n FROM Method TO CodeElement,\n FROM `Template` TO `Template`,\n FROM `Template` TO Function,\n FROM `Template` TO Method,\n FROM `Template` TO Class,\n FROM `Template` TO `Struct`,\n FROM `Template` TO `TypeAlias`,\n FROM `Template` TO `Enum`,\n FROM `Template` TO `Macro`,\n FROM `Template` TO Interface,\n FROM `Template` TO `Constructor`,\n FROM `Module` TO CodeElement,\n FROM `Module` TO `Namespace`,\n FROM `Namespace` TO Function,\n FROM CodeElement TO CodeElement,\n FROM CodeElement TO Class,\n FROM CodeElement TO Category,\n FROM CodeElement TO Method,\n FROM CodeElement TO `Module`,\n FROM CodeElement TO `Property`,\n FROM Section TO Section,\n FROM Interface TO CodeElement,\n FROM `Namespace` TO `Struct`,\n FROM `Impl` TO Method,\n FROM `Impl` TO Function,\n FROM `Impl` TO `Constructor`,\n FROM `Impl` TO `Property`,\n FROM `Impl` TO `Trait`,\n FROM `Impl` TO `Struct`,\n FROM `Impl` TO `Impl`,\n FROM `Constructor` TO `Template`,\n FROM `Constructor` TO `Impl`,\n FROM `Constructor` TO `Namespace`,\n FROM `Constructor` TO `Typedef`,\n FROM `Constructor` TO CodeElement,\n FROM Route TO Process,\n FROM Tool TO Process,\n FROM Destination TO `Property`,\n FROM BasicBlock TO BasicBlock";
|
|
100
102
|
export declare const RELATION_SCHEMA: string;
|
|
101
103
|
export declare const EMBEDDING_DIMS: number;
|
|
102
104
|
/** HNSW vector index name for the CodeEmbedding table. */
|
package/dist/core/lbug/schema.js
CHANGED
|
@@ -485,7 +485,7 @@ const ATTACHMENT_TARGET_LABELS = [
|
|
|
485
485
|
'Record',
|
|
486
486
|
];
|
|
487
487
|
/**
|
|
488
|
-
* The
|
|
488
|
+
* The pairs NEITHER rule above generates — everything left after the two
|
|
489
489
|
* cross products are subtracted. Carried by CONTAINMENT, inheritance, imports
|
|
490
490
|
* and DI: a container label crossed with a contained label. No predicate
|
|
491
491
|
* describes that surface (any container can hold any definition).
|
|
@@ -498,6 +498,8 @@ const ATTACHMENT_TARGET_LABELS = [
|
|
|
498
498
|
* templates), the two `Route|Process` / `Tool|Process` entry points whose
|
|
499
499
|
* emitter names both labels as literals, and `BasicBlock|BasicBlock`, the PDG
|
|
500
500
|
* substrate.
|
|
501
|
+
* Spring dynamic lookup in a constructor also emits INJECTS to a synthetic
|
|
502
|
+
* @Bean CodeElement (#3238), so Constructor|CodeElement belongs here.
|
|
501
503
|
*
|
|
502
504
|
* NOTHING A RULE ALREADY COVERS BELONGS HERE. `generatedRelationPairs` skips
|
|
503
505
|
* any pair present in this block, so a redundant line does not merely duplicate
|
|
@@ -591,6 +593,7 @@ export const STRUCTURAL_PAIR_DDL = ` FROM File TO Folder,
|
|
|
591
593
|
FROM \`Constructor\` TO \`Impl\`,
|
|
592
594
|
FROM \`Constructor\` TO \`Namespace\`,
|
|
593
595
|
FROM \`Constructor\` TO \`Typedef\`,
|
|
596
|
+
FROM \`Constructor\` TO CodeElement,
|
|
594
597
|
FROM Route TO Process,
|
|
595
598
|
FROM Tool TO Process,
|
|
596
599
|
FROM Destination TO \`Property\`,
|
package/package.json
CHANGED