mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* fix(swift): match repeated SPM target prefixes * test(swift): cover repeated SPM target prefixes * test(swift): cover valid prefix before later partial match * docs(swift): clarify target grouping parity scope * docs(swift): clarify target grouping parity scope * docs(swift): clarify target grouping parity scope * fix(swift): resolve imports from Package.swift targets, not path segments Stop fabricating IMPORTS from import Foundation onto a same-named folder. Declare modules from Package.swift when the manifest is usable; keep Sources/* for grouping and fail-open folder resolve minus SDK names. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(swift): keep empty Package.swift declarations and nested .target() deps external An inferred Sources/* folder is grouping-only. A dependency .target(name:) is not a module. Treat both as unresolved so import Foundation cannot bind to a decoy folder. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(swift): keep implicit IMPORTS intra-group and gate linear Package.swift resolve @_exported must not paint sibling files as implicit imports. A dedicated bench pins declaration-only resolve and (t_4n/t_n)/4 linearity. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3105) Honor member-only @_exported imports, skip comments while scanning Package.swift factories, fail-open mixed helper-built target lists, and block CoreData/CoreGraphics decoy folders on the inferred path. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3105) Match path: "." as the package root, skip block-commented Package.swift factories, read import kind from the clause only, and skip capture tests when the optional Swift grammar is missing. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ci): scan Swift import-kind without nested regex backtracking CodeQL js/redos flagged the comment-skipping IMPORT_KIND_RE; a linear walk keeps the same kind tokens. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ci): linear Package.swift factory scan and gate Swift context parseSwiftPackageManifest re-walked every prefix for comments (O(n²) in factory count). Resume the scan and cover nested factories in one pass. Wire Swift into the import-target context arm now that resolveImportTarget is 5-arg. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3105) Tighten Package.swift and import-text scanners: skip comments/strings, reject escapes, treat ident + [ as incomplete, and drop the unused factory-comment wrapper. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ci): prettier the @_exported availability fixture Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3105) Judge Package.swift completeness from Package(...)'s own targets: argument instead of raw-text regexes, nest block comments when reading an import kind, and strip leading ./ from declared target paths. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3105) Collect Package.swift factories only from Package(targets: [...]), fail-open on computed array elements, and treat // after a label colon as a comment. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3105) Ignore stray factories when Package() exists but omits targets:. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3105) Require the Package-scan seen box so the always-true undefined guard goes away. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
530 lines
19 KiB
TypeScript
530 lines
19 KiB
TypeScript
/**
|
|
* One property, asserted for EVERY registered `ScopeResolver` (#2953).
|
|
*
|
|
* A specifier that names something OUTSIDE the repository must not resolve to
|
|
* a file inside it.
|
|
*
|
|
* This is the property #2953 was filed against. Its violation is not a missing
|
|
* edge but a fabricated one: `@acme/telemetry/nest` resolving to the repo's
|
|
* only path ending in `nest/index.ts` produces an `IMPORTS` edge at full
|
|
* confidence between two files that have no relationship, and `impact` then
|
|
* reports a blast radius into code that cannot be affected. The reporter
|
|
* measured 44 of 74 cross-package edges landing on two such files.
|
|
*
|
|
* The mechanism is shared, which is why this file is: `import-resolvers/
|
|
* utils.ts:suffixResolve` asks "does any file in this repo have a path ending
|
|
* in this specifier?", retrying with each leading segment dropped, and most
|
|
* languages still end their resolution chain there.
|
|
*
|
|
* ## The decoy is the whole test
|
|
*
|
|
* Every case pairs an external specifier with a DECOY — an unrelated in-repo
|
|
* file whose path ends the way the specifier does. Without one, a resolver that
|
|
* simply found nothing would pass while still holding no property at all: the
|
|
* question is not "did it return null" but "did it return null when a tempting
|
|
* wrong answer was sitting in the file set". Each case therefore also asserts
|
|
* the decoy is genuinely reachable, by resolving the spelling that SHOULD find
|
|
* it, so a typo in a fixture cannot manufacture a pass.
|
|
*
|
|
* ## `KNOWN_GAPS` is a work list, not an allowance
|
|
*
|
|
* A language in that map does not hold the property today. The entry records
|
|
* what its resolver currently answers, so the follow-up that fixes it deletes
|
|
* one line and the arm below starts enforcing it. Both arms run for every
|
|
* language either way — a gap that silently closes fails just as loudly as one
|
|
* that opens, because a resolver quietly starting to answer `null` for the
|
|
* paired positive case is a regression wearing the fix's clothes.
|
|
*/
|
|
import { describe, expect, it } from 'vitest';
|
|
import type { ParsedFile, ParsedImport, ScopeId, SymbolDefinition } from 'gitnexus-shared';
|
|
import { SupportedLanguages } from 'gitnexus-shared';
|
|
import { SCOPE_RESOLVERS } from '../../../src/core/ingestion/scope-resolution/pipeline/registry.js';
|
|
import type { ComposerConfig } from '../../../src/core/ingestion/language-config.js';
|
|
import {
|
|
clearJavaPackageFacts,
|
|
setJavaPackageFact,
|
|
} from '../../../src/core/ingestion/languages/java/package-facts.js';
|
|
import {
|
|
clearKotlinPackageFacts,
|
|
setKotlinPackageFact,
|
|
} from '../../../src/core/ingestion/languages/kotlin/package-facts.js';
|
|
|
|
/** The `composer.json` PSR-4 map `loadPhpComposerConfig` would have produced. */
|
|
const PHP_COMPOSER: ComposerConfig = { psr4: new Map([['App', 'app']]) };
|
|
/** The value `loadGoModulePath` produces for a repo with a `go.mod`. */
|
|
const GO_MODULE = { modulePath: 'example.com/mod' };
|
|
/** The root dependency scope `loadRubyResolutionConfig` would have produced. */
|
|
const RUBY_GEMS = {
|
|
scopesByDirectory: new Map([
|
|
[
|
|
'',
|
|
{
|
|
externalRequirePrefixes: new Set(['rails']),
|
|
localLoadRootsByPrefix: new Map(),
|
|
},
|
|
],
|
|
]),
|
|
};
|
|
/** What `scanCSharpProject` would report for the C# workspace below — the
|
|
* in-repo namespace evidence the #1881 suffix-fallback gate reads. */
|
|
const CSHARP_NAMESPACES = {
|
|
namespaces: {
|
|
declaredNamespaces: new Set(['App', 'App.Models']),
|
|
rootNamespaces: new Set(['App']),
|
|
truncated: false,
|
|
},
|
|
};
|
|
|
|
interface ConformanceCase {
|
|
/** The workspace, including the decoy. */
|
|
readonly files: readonly string[];
|
|
readonly fromFile: string;
|
|
readonly resolutionConfig: unknown;
|
|
/** A specifier naming something outside the repo. Must resolve to nothing. */
|
|
readonly external: string;
|
|
/**
|
|
* The in-repo file the external specifier is tempting: its path ends the way
|
|
* `external` does, so a suffix matcher lands on it.
|
|
*/
|
|
readonly decoy: string;
|
|
/**
|
|
* A specifier that SHOULD reach {@link decoy} — the paired positive proving
|
|
* the decoy is live, so a `null` below is a refusal rather than an empty
|
|
* corpus.
|
|
*
|
|
* Required only for a language that HOLDS the property. For one in
|
|
* {@link KNOWN_GAPS} the gap assertion already resolves `external` TO the
|
|
* decoy, which is that proof — and for several of them no other spelling
|
|
* exists: Swift's `Foundation` and COBOL's `EXTERNAL` name the in-repo
|
|
* directory and copybook as well as the external module, which is precisely
|
|
* why those resolvers cannot tell the two apart.
|
|
*/
|
|
readonly reachesDecoy?: string;
|
|
readonly parsedImport?: (targetRaw: string) => ParsedImport | undefined;
|
|
/**
|
|
* Declarations the resolver reads from the parsed workspace rather than from
|
|
* path shape. Java resolves entirely this way since #2953 — a specifier names
|
|
* a type in a DECLARED package — so a case that supplied only paths would
|
|
* measure a resolver with no workspace at all, and every arm would pass for
|
|
* the wrong reason.
|
|
*/
|
|
readonly declare?: () => void;
|
|
readonly parsedFile?: (filePath: string) => ParsedFile;
|
|
}
|
|
|
|
function kotlinParsedFile(filePath: string): ParsedFile {
|
|
const name = filePath.slice(filePath.lastIndexOf('/') + 1, filePath.lastIndexOf('.'));
|
|
const moduleScope = `module:${filePath}` as ScopeId;
|
|
const def: SymbolDefinition = {
|
|
nodeId: `Class:${filePath}:${name}`,
|
|
filePath,
|
|
type: 'Class',
|
|
qualifiedName: name,
|
|
};
|
|
return {
|
|
filePath,
|
|
moduleScope,
|
|
scopes: [
|
|
{
|
|
id: moduleScope,
|
|
parent: null,
|
|
kind: 'Module',
|
|
range: { startLine: 1, startCol: 0, endLine: 1, endCol: 1 },
|
|
filePath,
|
|
bindings: new Map([[name, [{ def, origin: 'local' }]]]),
|
|
ownedDefs: [def],
|
|
imports: [],
|
|
typeBindings: new Map(),
|
|
},
|
|
],
|
|
parsedImports: [],
|
|
localDefs: [def],
|
|
referenceSites: [],
|
|
};
|
|
}
|
|
|
|
const PHP_FUNCTION_IMPORT = (targetRaw: string): ParsedImport => ({
|
|
kind: 'named',
|
|
localName: 'imported',
|
|
importedName: 'imported',
|
|
targetRaw,
|
|
importedSymbolKind: 'function',
|
|
});
|
|
|
|
const CASES: ReadonlyMap<SupportedLanguages, ConformanceCase> = new Map([
|
|
[
|
|
SupportedLanguages.TypeScript,
|
|
{
|
|
files: ['apps/web/src/main.ts', 'packages/inner/src/nest/index.ts', 'apps/web/src/nest.ts'],
|
|
fromFile: 'apps/web/src/main.ts',
|
|
resolutionConfig: undefined,
|
|
external: '@acme/telemetry/nest',
|
|
decoy: 'packages/inner/src/nest/index.ts',
|
|
reachesDecoy: '../../../packages/inner/src/nest',
|
|
},
|
|
],
|
|
[
|
|
SupportedLanguages.JavaScript,
|
|
{
|
|
files: ['apps/web/src/main.js', 'packages/inner/src/nest/index.js'],
|
|
fromFile: 'apps/web/src/main.js',
|
|
resolutionConfig: undefined,
|
|
external: '@acme/telemetry/nest',
|
|
decoy: 'packages/inner/src/nest/index.js',
|
|
reachesDecoy: '../../../packages/inner/src/nest',
|
|
},
|
|
],
|
|
[
|
|
SupportedLanguages.Vue,
|
|
{
|
|
files: ['apps/web/src/main.ts', 'packages/inner/src/nest/index.ts'],
|
|
fromFile: 'apps/web/src/main.ts',
|
|
resolutionConfig: undefined,
|
|
external: '@acme/telemetry/nest',
|
|
decoy: 'packages/inner/src/nest/index.ts',
|
|
reachesDecoy: '../../../packages/inner/src/nest',
|
|
},
|
|
],
|
|
[
|
|
SupportedLanguages.Python,
|
|
{
|
|
// #898's exact shape: `django.apps` beside an unrelated `accounts/apps.py`.
|
|
files: ['accounts/apps.py', 'accounts/models.py', 'billing/models.py'],
|
|
fromFile: 'accounts/models.py',
|
|
resolutionConfig: undefined,
|
|
external: 'django.apps',
|
|
decoy: 'accounts/apps.py',
|
|
reachesDecoy: 'accounts.apps',
|
|
},
|
|
],
|
|
[
|
|
SupportedLanguages.CSharp,
|
|
{
|
|
// #1881's shape: a BCL using beside a same-named local file.
|
|
files: ['App/Tasks.cs', 'App/Models/User.cs', 'App/Program.cs'],
|
|
fromFile: 'App/Program.cs',
|
|
// The #1881 gate is evidence-driven and fails OPEN without any, so a
|
|
// `undefined` config here would record a gap C# does not have.
|
|
resolutionConfig: CSHARP_NAMESPACES,
|
|
external: 'System.Threading.Tasks',
|
|
decoy: 'App/Tasks.cs',
|
|
reachesDecoy: 'App.Tasks',
|
|
},
|
|
],
|
|
[
|
|
SupportedLanguages.Java,
|
|
{
|
|
files: [
|
|
'vendor/util/List.java',
|
|
'com/example/model/User.java',
|
|
'src/main/java/com/example/App.java',
|
|
],
|
|
fromFile: 'src/main/java/com/example/App.java',
|
|
resolutionConfig: undefined,
|
|
// `vendor/util/List.java` declares package `vendor.util`, so
|
|
// `vendor.util.List` is a real import of it and `java.util.List` is not.
|
|
// The two differ ONLY by what the workspace declares — both specifiers
|
|
// end in `util.List`, and both would match the same path suffix.
|
|
declare: () => {
|
|
clearJavaPackageFacts();
|
|
setJavaPackageFact('vendor/util/List.java', {
|
|
status: 'known',
|
|
packageName: 'vendor.util',
|
|
});
|
|
setJavaPackageFact('com/example/model/User.java', {
|
|
status: 'known',
|
|
packageName: 'com.example.model',
|
|
});
|
|
setJavaPackageFact('src/main/java/com/example/App.java', {
|
|
status: 'known',
|
|
packageName: 'com.example',
|
|
});
|
|
},
|
|
external: 'java.util.List',
|
|
decoy: 'vendor/util/List.java',
|
|
reachesDecoy: 'vendor.util.List',
|
|
},
|
|
],
|
|
[
|
|
SupportedLanguages.Kotlin,
|
|
{
|
|
files: ['src/main/kotlin/vendor/Assert.kt', 'src/main/kotlin/com/example/App.kt'],
|
|
fromFile: 'src/main/kotlin/com/example/App.kt',
|
|
resolutionConfig: undefined,
|
|
declare: () => {
|
|
clearKotlinPackageFacts();
|
|
setKotlinPackageFact('src/main/kotlin/vendor/Assert.kt', {
|
|
status: 'known',
|
|
packageName: 'vendor',
|
|
});
|
|
setKotlinPackageFact('src/main/kotlin/com/example/App.kt', {
|
|
status: 'known',
|
|
packageName: 'com.example',
|
|
});
|
|
},
|
|
parsedFile: kotlinParsedFile,
|
|
external: 'org.junit.Assert',
|
|
decoy: 'src/main/kotlin/vendor/Assert.kt',
|
|
reachesDecoy: 'vendor.Assert',
|
|
},
|
|
],
|
|
[
|
|
SupportedLanguages.Go,
|
|
{
|
|
files: ['internal/models/user.go', 'main.go'],
|
|
fromFile: 'main.go',
|
|
resolutionConfig: GO_MODULE,
|
|
external: 'github.com/vendor/dep/internal/models',
|
|
decoy: 'internal/models/user.go',
|
|
reachesDecoy: 'example.com/mod/internal/models',
|
|
},
|
|
],
|
|
[
|
|
SupportedLanguages.Ruby,
|
|
{
|
|
files: ['lib/app/models/user.rb', 'lib/generators.rb', 'lib/main.rb'],
|
|
fromFile: 'lib/main.rb',
|
|
resolutionConfig: RUBY_GEMS,
|
|
external: 'rails/generators',
|
|
decoy: 'lib/generators.rb',
|
|
reachesDecoy: 'generators',
|
|
},
|
|
],
|
|
[
|
|
SupportedLanguages.Rust,
|
|
{
|
|
files: ['src/de.rs', 'src/models.rs', 'src/main.rs'],
|
|
fromFile: 'src/main.rs',
|
|
resolutionConfig: undefined,
|
|
external: 'serde::de',
|
|
decoy: 'src/de.rs',
|
|
reachesDecoy: 'crate::de',
|
|
},
|
|
],
|
|
[
|
|
SupportedLanguages.PHP,
|
|
{
|
|
files: ['app/Ghost/Missing.php', 'app/Models/User.php', 'app/Main.php'],
|
|
fromFile: 'app/Main.php',
|
|
resolutionConfig: PHP_COMPOSER,
|
|
external: 'Vendor\\Ghost\\Missing',
|
|
decoy: 'app/Ghost/Missing.php',
|
|
reachesDecoy: 'App\\Ghost\\Missing',
|
|
parsedImport: PHP_FUNCTION_IMPORT,
|
|
},
|
|
],
|
|
[
|
|
SupportedLanguages.Dart,
|
|
{
|
|
files: ['lib/http.dart', 'lib/models.dart', 'lib/main.dart'],
|
|
fromFile: 'lib/main.dart',
|
|
resolutionConfig: undefined,
|
|
external: 'package:http/http.dart',
|
|
decoy: 'lib/http.dart',
|
|
reachesDecoy: 'package:app/http.dart',
|
|
},
|
|
],
|
|
[
|
|
SupportedLanguages.Swift,
|
|
{
|
|
files: [
|
|
'Sources/Foundation/Thing.swift',
|
|
'Sources/Models/User.swift',
|
|
'Sources/App/main.swift',
|
|
],
|
|
fromFile: 'Sources/App/main.swift',
|
|
resolutionConfig: undefined,
|
|
external: 'Foundation',
|
|
decoy: 'Sources/Foundation/Thing.swift',
|
|
reachesDecoy: 'Sources',
|
|
},
|
|
],
|
|
[
|
|
SupportedLanguages.C,
|
|
{
|
|
files: ['src/stdio.h', 'include/util.h', 'src/main.c'],
|
|
fromFile: 'src/main.c',
|
|
resolutionConfig: undefined,
|
|
external: 'stdio.h',
|
|
decoy: 'src/stdio.h',
|
|
reachesDecoy: 'util.h',
|
|
},
|
|
],
|
|
[
|
|
SupportedLanguages.CPlusPlus,
|
|
{
|
|
files: ['src/cstdio.h', 'include/util.hpp', 'src/main.cpp'],
|
|
fromFile: 'src/main.cpp',
|
|
resolutionConfig: undefined,
|
|
// `cstdio` with no extension would miss the decoy on spelling alone and
|
|
// post a pass that measures nothing; the header spelling is the real test.
|
|
external: 'cstdio.h',
|
|
decoy: 'src/cstdio.h',
|
|
reachesDecoy: 'util.hpp',
|
|
},
|
|
],
|
|
[
|
|
SupportedLanguages.Cobol,
|
|
{
|
|
files: ['copybooks/CUSTREC.cpy', 'vendor/EXTERNAL.cpy', 'src/PROG.cbl'],
|
|
fromFile: 'src/PROG.cbl',
|
|
resolutionConfig: undefined,
|
|
external: 'EXTERNAL',
|
|
// After the copybook-dir preference, vendor/EXTERNAL.cpy is intentionally
|
|
// unreachable (that is the #2967 fix). The reachable decoy is the in-repo
|
|
// copybook; vendor/EXTERNAL.cpy stays in `files` so EXTERNAL→[] is not a
|
|
// vacuous miss of an empty workspace.
|
|
decoy: 'copybooks/CUSTREC.cpy',
|
|
reachesDecoy: 'CUSTREC',
|
|
},
|
|
],
|
|
[
|
|
SupportedLanguages.Zig,
|
|
{
|
|
// `@import("std")` is the standard library, and Zig's resolver answers
|
|
// null for the stdlib names outright — it never suffix-matches a bare
|
|
// name against the file set, so a repo file that happens to be called
|
|
// `std.zig` is not a candidate. The same file IS reachable through the
|
|
// filesystem-relative spelling, which is what the decoy arm proves.
|
|
files: ['src/std.zig', 'src/util.zig', 'src/main.zig'],
|
|
fromFile: 'src/main.zig',
|
|
resolutionConfig: undefined,
|
|
external: 'std',
|
|
decoy: 'src/std.zig',
|
|
reachesDecoy: 'std.zig',
|
|
},
|
|
],
|
|
[
|
|
SupportedLanguages.ObjectiveC,
|
|
{
|
|
files: ['Headers/Foundation.h', 'Headers/Widget.h', 'Sources/main.m'],
|
|
fromFile: 'Sources/main.m',
|
|
resolutionConfig: undefined,
|
|
external: 'Foundation',
|
|
decoy: 'Headers/Foundation.h',
|
|
reachesDecoy: 'Foundation.h',
|
|
},
|
|
],
|
|
]);
|
|
|
|
/**
|
|
* Languages that do NOT hold the property yet, with what they answer instead.
|
|
*
|
|
* Each entry is a follow-up, not a decision: the resolver ends its chain in
|
|
* `suffixResolve` and so cannot tell an external module from a path suffix.
|
|
* Fixing one means giving that language its real algorithm the way #2953 gave
|
|
* TypeScript one, then deleting its line here.
|
|
*/
|
|
const KNOWN_GAPS: ReadonlyMap<SupportedLanguages, string> = new Map<SupportedLanguages, string>([
|
|
[SupportedLanguages.Dart, '`package:http/http.dart` -> `lib/http.dart`'],
|
|
[SupportedLanguages.C, '`stdio.h` -> `src/stdio.h`'],
|
|
[SupportedLanguages.CPlusPlus, '`cstdio.h` -> `src/cstdio.h`'],
|
|
]);
|
|
|
|
/**
|
|
* The six that hold it, and what earns each one.
|
|
*
|
|
* Not a list to maintain by hand — it is derived below — but worth reading
|
|
* once, because the three mechanisms are different and only one of them
|
|
* generalizes:
|
|
*
|
|
* - TypeScript / JavaScript / Vue resolve against declared config only and
|
|
* have no suffix fallback at all (#2953). This is the shape the ten above
|
|
* need.
|
|
* - Python (#898) and C# (#1881) kept the fallback and put a gate in front of
|
|
* it, keyed on whether the specifier's leading segment names anything
|
|
* in-repo. Cheaper, and it holds — but it is a filter on a guess rather
|
|
* than a resolution rule, so it answers "probably not external" instead of
|
|
* "here is what the language says this means".
|
|
* - Rust holds it for a spelling reason: `::` is not `/` or `.`, so
|
|
* `serde::de` never decomposes into a path suffix that could match
|
|
* `src/de.rs`. The decoy-reachability arm proves the file IS reachable via
|
|
* `crate::de`, so this is a real pass — but it is contingent on the
|
|
* separator, not on Rust knowing what a crate is.
|
|
*/
|
|
|
|
function resolveWith(
|
|
language: SupportedLanguages,
|
|
testCase: ConformanceCase,
|
|
targetRaw: string,
|
|
): string | readonly string[] | null {
|
|
const resolver = SCOPE_RESOLVERS.get(language)!;
|
|
const files = new Set(testCase.files);
|
|
testCase.declare?.();
|
|
// The parsed workspace, supplied only to a case that declares one. A minimal
|
|
// `{ filePath }` stand-in is exactly right for a resolver that reads the file
|
|
// list plus its own fact store (Java), and WRONG for one that reads other
|
|
// ParsedFile fields (PHP's `filesByDirectory`), which would silently resolve
|
|
// differently against stubs than against real parsed files.
|
|
const parsedFiles =
|
|
testCase.declare === undefined
|
|
? []
|
|
: testCase.files.map(
|
|
testCase.parsedFile ?? ((filePath) => ({ filePath }) as unknown as ParsedFile),
|
|
);
|
|
return resolver.resolveImportTarget(
|
|
targetRaw,
|
|
testCase.fromFile,
|
|
files,
|
|
testCase.resolutionConfig,
|
|
{
|
|
parsedFiles,
|
|
parsedImport: testCase.parsedImport?.(targetRaw),
|
|
},
|
|
);
|
|
}
|
|
|
|
/** Every file an answer names, whatever shape the resolver used. */
|
|
function filesOf(answer: string | readonly string[] | null): readonly string[] {
|
|
if (answer === null) return [];
|
|
return typeof answer === 'string' ? [answer] : answer;
|
|
}
|
|
|
|
describe('external imports never resolve into the repository (#2953)', () => {
|
|
it('covers every registered scope resolver', () => {
|
|
// A new language lands here before it lands anywhere else: it either gets a
|
|
// case or an explicit gap entry, and both are edits someone has to justify.
|
|
expect([...CASES.keys()].sort()).toEqual([...SCOPE_RESOLVERS.keys()].sort());
|
|
});
|
|
|
|
it.each([...CASES.keys()].filter((language) => !KNOWN_GAPS.has(language)))(
|
|
'%s: the decoy is reachable, so the arm below means something',
|
|
(language) => {
|
|
const testCase = CASES.get(language)!;
|
|
const reached = filesOf(resolveWith(language, testCase, testCase.reachesDecoy!));
|
|
|
|
// The DECOY specifically, not merely something. Asserting non-empty let a
|
|
// case pair `reachesDecoy` with a different file than `decoy` and still
|
|
// pass, which proves the resolver can reach SOME file and says nothing
|
|
// about whether the tempting wrong answer below was ever reachable.
|
|
expect(
|
|
reached,
|
|
`${language}: '${testCase.reachesDecoy}' did not reach the decoy '${testCase.decoy}', so this workspace proves nothing about '${testCase.external}'`,
|
|
).toContain(testCase.decoy);
|
|
},
|
|
);
|
|
|
|
it.each([...CASES.keys()])('%s: an external specifier resolves to nothing', (language) => {
|
|
const testCase = CASES.get(language)!;
|
|
const resolved = filesOf(resolveWith(language, testCase, testCase.external));
|
|
const gap = KNOWN_GAPS.get(language);
|
|
|
|
if (gap !== undefined) {
|
|
// The gap is asserted, not skipped, and asserted as the RECORDED answer
|
|
// rather than as "something": a resolver returning an unrelated in-repo
|
|
// file would otherwise keep the entry green while the map's description
|
|
// of what it does went stale. A language that starts holding the property
|
|
// fails here too, which is how the entry gets deleted deliberately.
|
|
expect(
|
|
resolved,
|
|
`${language}: KNOWN_GAPS records '${testCase.external}' resolving to '${testCase.decoy}', but it resolved to ${JSON.stringify(resolved)} — update the entry or delete it`,
|
|
).toEqual([testCase.decoy]);
|
|
return;
|
|
}
|
|
|
|
expect(
|
|
resolved,
|
|
`${language}: '${testCase.external}' names nothing in this repo, but resolved to ${JSON.stringify(resolved)}`,
|
|
).toEqual([]);
|
|
});
|
|
});
|