/** * Shared path matching utilities for dependency analysis. * * These functions handle path normalization and matching logic used by * the get_dependents tool to find reverse dependencies. */ /** * Normalizes a file path for comparison. * * - Removes quotes and trims whitespace * - Converts backslashes to forward slashes * - Strips file extensions for all AST-supported languages * - Converts absolute paths to relative (if within workspace root) * * @param path - The path to normalize * @param workspaceRoot - The workspace root directory (normalized with forward slashes) * @returns Normalized path */ export declare function normalizePath(path: string, workspaceRoot: string): string; /** * Creates a cached `normalizePath` wrapper, to avoid repeating the same string * work for a path (or import specifier) seen many times in one analysis pass. * * Lives here rather than in `dependency-analyzer.ts` (its original home) so the * batch reverse-dependency pass in `dependent-count-index.ts` shares the exact * same normalizer construction as `analyzeDependencies`/`findDependents` -- * `importMatchesTarget` takes the caller's normalizer as a parameter, and two * call sites building it differently is precisely how a "same decision, * implemented at N sites" divergence starts. * * @param workspaceRoot - The workspace root directory for path normalization * @returns A function that normalizes and caches file paths */ export declare function createPathNormalizer(workspaceRoot: string): (path: string) => string; /** * Checks if a pattern matches at path component boundaries. * * Ensures matches occur at proper path boundaries (/) to avoid false positives like: * - "logger" matching "logger-utils" ❌ * - "src/logger" matching "src/logger-service" ❌ * * @param str - The string to search in * @param pattern - The pattern to search for * @returns True if pattern matches at a boundary */ export declare function matchesAtBoundary(str: string, pattern: string): boolean; /** * Drop the memoized importer-language records. Exported for tests that need * to prove a cold cache and a warm cache give the same answer; production * code never needs it (the registry the cache derives from is immutable). */ export declare function clearImporterSemanticsCache(): void; /** * True when `importSpecifier` is a bare (slash-free) import from a file whose * language sets `LanguageDefinition.wholeModuleImports` (Swift today — see * `hasWholeModuleImports`'s doc comment for #869's structural background). * * `matchesFile` is deliberately language-agnostic: it only ever sees raw * import/target strings, never a language tag. But for a whole-module-import * language, every extracted import IS the bare module name (`SwiftImport * Extractor` never emits a per-file specifier), so the *only* way such an * import can ever "win" a `matchesFile` comparison is through strategy 2's * one-leading-segment leniency (`auth` -> `src/auth.rs`) firing purely * because a target file's basename happens to coincide with the module's own * name (`Source/Alamofire.swift` vs. `import Alamofire`) -- the exact #884 * false-hub shape, one leading segment inside the window #868/#883 * deliberately preserve for the legitimate Rust-style convention. * * Match-side callers (does this import resolve to this target?) should go * through `importMatchesTarget` below, which applies this guard before * calling `matchesFile` so the two can never drift apart (#886). Build-side * callers with no target in scope (`buildImportIndex`, * `indexImportEntry`/`addChunkToImportIndex`) have nothing for * `importMatchesTarget` to compare against, so they keep calling this * predicate directly to decide whether an (import, chunk) pair is worth * indexing at all -- the honest outcome when it's true is #869's "not * determinable" signal, never a match. This is intentionally the only place * that combines path-matching with language data; `matchesAtBoundaryPrecise`'s * general guard stays untouched and keeps serving every non-whole-module * language (Rust, Go, Ruby, ...) exactly as before. * * @param importSpecifier - The raw (pre-normalization) import specifier * @param importerFile - File path of the chunk doing the importing */ export declare function isUnresolvableWholeModuleImport(importSpecifier: string, importerFile: string): boolean; /** * Determines if an import path matches a target file path. * * Handles various matching strategies: * 1. Exact match * 2. Target path appears in import (at boundaries) * 3. Import path appears in target (at boundaries) * 4. Relative imports (./logger vs src/utils/logger) * 5. PHP namespace imports (App\Models\User vs app/Models/User.php) * 6. Python module imports (django.http → django/http/__init__.py or django/http/*.py) * * `matchesFile` itself stays language-agnostic (see `isUnresolvableWholeModuleImport`'s * doc comment) — it never inspects `importerFile` or detects a language. But * strategies 1/2's multi-segment boundary check has one genuine language- * dependent fork (#887): does a bare multi-segment specifier name a single * file (Ruby) or a package directory whose files are all members (Go)? A * language-agnostic caller can't know, so it's threaded in as an explicit * parameter rather than decided here — see `requireExactTailForMultiSegment` * and `importMatchesTarget`, the only caller that derives it from the * importer's language. Every other caller passes the default (`false`, * permissive/Go-safe), preserving this function's pre-#887 behavior exactly. * * Strategy 5 has its own, narrower language fork (#929): `matchesPythonModule`'s * bare-specifier branch treats a resolved single-segment specifier as a * Python package import, matching every file nested anywhere underneath it * (`matchesParentPythonPackage`'s unbounded `startsWith`, no depth cap at * all -- unlike every other strategy here, which anchors both edges of the * match). That is a real Python semantic, but `matchesFile` used to run it * unconditionally for every language, and a resolved bare specifier can * coincidentally look exactly like a Python identifier in any language -- * confirmed on a real TypeScript repo (hono), where a test's own package-root * barrel import (`import { Hono } from '../..'`, resolved to the bare * specifier `src`) satisfied `matchesParentPythonPackage('src', 'src/utils/ * jwt/jws')` for every single file under `src/`, fabricating "this test * covers everything" for a bare barrel import with no real relationship to * the target. See `allowPythonModuleMatching` and `importMatchesTarget`, * the only caller that derives it from the importer's language. Every other * caller passes the default (`true`), preserving this function's pre-#929 * behavior exactly -- this is deliberately scoped to `importMatchesTarget`'s * match-side callers, mirroring #887's precedent, not to `matchesFile`'s * remaining direct callers (existing Python fixtures, `buildReExportGraph`'s * self-skip check -- see `importMatchesTarget`'s doc comment for why * `findDependentChunks`'s fuzzy loop no longer belongs on this list as of * #994 Phase 3). * * Strategy 4 has the identical per-language shape (#1028): `matchesPHPNamespace` * is a real semantic for PHP's case-insensitive, directory-mirroring PSR-4 * namespaces, but `matchesFile` used to run it unconditionally for every * language too. Its bare-single-component branch's case-insensitivity (added * by #883 for an unrelated Swift fix) let a Rust bare `use crate::{Error}` * specifier (the import extractor's "first wins" grouped-use handling) * case-insensitively self-match `src/error.rs` on a real `dtolnay/anyhow` * clone -- confirmed for three files (`chain.rs`/`context.rs`/`error.rs`), * each via a self-referential bare `use crate::;` (grouped or not -- * only `chain.rs`'s is `pub(crate)` and ungrouped; `error.rs`/`context.rs` * are plain grouped `use crate::{...}`) naming its own type. See * `allowNamespaceMatching` and `importMatchesTarget`, the only * caller that derives it from the importer's language via * `hasNamespaceMatchingSemantics`. Every other caller passes the default * (`true`), preserving this function's pre-#1028 behavior exactly, mirroring * `allowPythonModuleMatching`'s own scoping precedent immediately above. * * @param normalizedImport - Normalized import path * @param normalizedTarget - Normalized target file path * @param requireExactTailForMultiSegment - When true, a multi-segment bare * pattern must reach the end of the compared string (Ruby's single-file * `require` semantics); when false (the default), a multi-segment bare * pattern may also match a "child" continuing past it (Go's package- * directory semantics, and the safe default for every other language). * @param allowPythonModuleMatching - When false, Strategy 5 (Python module * matching) is skipped entirely. Defaults to `true` (this function's * pre-#929 behavior); `importMatchesTarget` passes `false` for any * non-Python importer -- see the doc comment above. * @param allowNamespaceMatching - When false, Strategy 4 (PHP namespace * matching) is skipped entirely. Defaults to `true` (this function's * pre-#1028 behavior); `importMatchesTarget` passes `false` for any * importer whose language doesn't set `namespaceStyleImports` -- see the * doc comment above. * @returns True if the import matches the target file */ export declare function matchesFile(normalizedImport: string, normalizedTarget: string, requireExactTailForMultiSegment?: boolean, allowPythonModuleMatching?: boolean, allowNamespaceMatching?: boolean): boolean; /** * The single guarded import-matching decision: does `importSpecifier` (as * written in `importerFile`) resolve to `normalizedTarget`? * * Couples five guards to `matchesFile` so neither can drift apart from a * match-side call site again: * - The #884 whole-module guard (`isUnresolvableWholeModuleImport`) -- * `matchesFile` is language-agnostic and cannot know the importer's * language, so this MUST run on the RAW specifier first. Spelled inline * here as `semantics.wholeModuleImports && !importSpecifier.includes('/')` * rather than as a call to that predicate, purely so the shared * `importerLanguageSemantics` lookup is done once for all four guards * (#1075); the two conjuncts are both pure, so testing the language flag * before the slash is the same decision in the other order, and * `isUnresolvableWholeModuleImport` remains the single definition every * OTHER call site (`buildImportIndex`, `indexImportEntry`, * `test-associations.ts`, `get-files-context.ts`) uses. * - The #887 single-file-vs-package-directory distinction * (`requireExactTailForMultiSegment`) -- derived from the importer's * language via `hasSingleFileImports`, since that's the only information * that can disambiguate a bare multi-segment specifier like `rack/protection` * (Ruby: names one file) from `internal/fs` (Go: names a package directory * whose files are all members). This is the ONE call site with both an * importer file *and* a target to compare against, so it's the only place * this derivation happens. * - The #929 Python-bare-module guard (`allowPythonModuleMatching`) -- * `matchesFile`'s Strategy 5 is a real Python semantic, but a false hub for * any other language whose resolved bare specifier coincidentally matches * a Python identifier shape (see `matchesFile`'s doc comment for the real * hono/TypeScript repro). Derived from the importer's language the same * way as the #887 guard, at this same call site. * - The #1028 PHP-namespace guard (`allowNamespaceMatching`) -- * `matchesFile`'s Strategy 4 is a real PHP PSR-4 semantic, but a false hub * for any other language whose resolved bare/qualified specifier * case-insensitively coincides with a target's basename (see `matchesFile`'s * doc comment for the real `dtolnay/anyhow` Rust self-edge repro). Derived * from the importer's language via `hasNamespaceMatchingSemantics`, the * same way as the #887/#929 guards, at this same call site. * - The #1021/#1056 Rust exact-single-file guard (`hasRustModMarker`) -- * unlike the other four, this is derived from the SPECIFIER, not the * importer's language: a single Rust file can have both a `mod x;` or a * bare crate-root import (each needing `matchesRustModSpecifier`'s strict * semantics) and a `use crate::y;` (needing `matchesFile`'s existing * leniency) among its own imports, so a per-language flag can't * disambiguate between two entries in the same file's import list the way * it can for #887/#929/#1028. When present, this guard short-circuits * entirely -- `matchesFile` never runs at all for a marked specifier. * * Every match-side reverse-dependency call path that used to open-code * `!isUnresolvableWholeModuleImport(imp, f) && matchesFile(normalize(imp), t)` * now goes through here instead (#886). Three call paths in * `dependency-analyzer.ts` used to be the exception, and are no longer (#994 * Phase 3): * * - The two build-side sites that index imports with no target in scope * (`buildImportIndex`, `indexImportEntry`/`addChunkToImportIndex`) still * call `isUnresolvableWholeModuleImport` directly at build time (that part * hasn't changed -- it's an early-drop optimization, and there's still no * target to compare against yet). What changed is what they store: each * index entry now keeps the raw (pre-normalization) specifier alongside its * chunk (`ImportIndexEntry`), instead of discarding it once the bucket key * is computed. * - `findDependentChunks`'s fuzzy loop (`addFuzzyMatchChunks`) used to have * nothing but a normalized specifier and a bare chunk list to work with, so * it reconstructed the #887/#929 guards itself via two extra `matchesFile` * calls per bucket. With `rawSpecifier` preserved on every entry, it now * calls `importMatchesTarget` directly, per entry -- the same primitive, * the same guards, no reconstruction. * * `buildReExportGraph` is unchanged and still not routed through here, for a * different reason than the other three: it never reads the import index at * all. Its own re-export detection (`fileIsReExporter` -> * `findReExportedSymbolsForFile` -> `collectImportedSymbolsFromSource`) * already calls `importMatchesTarget` (that was already true before #994). * The one raw `matchesFile` call left in `buildReExportGraph` itself is a * same-normalizer FILE-vs-FILE identity check (skip the target file when * scanning candidates), not an import-vs-file match -- there is no * `importSpecifier` in that comparison for this primitive to guard, so it * was never a candidate for routing through it in the first place. * * @param importSpecifier - The raw (pre-normalization) import specifier, or * an `importedSymbols` key (same shape). * @param importerFile - File path of the chunk doing the importing (needed * for all three guards' language detection). * @param normalizedTarget - The already-normalized target path to compare * against. * @param normalize - The caller's own cached `normalizePath` wrapper. */ export declare function importMatchesTarget(importSpecifier: string, importerFile: string, normalizedTarget: string, normalize: (p: string) => string): boolean; /** * True when `importerFile`'s language sets `LanguageDefinition.singleFileImports` * (Ruby today) -- see that flag's doc comment for the Ruby-vs-Go distinction * this drives. Until #994 Phase 3, this was also called directly by * `findDependentChunks`'s own per-chunk #887 reconstruction (see git history * on `addFuzzyMatchChunks`), specifically so the two computations couldn't * drift apart. `findDependentChunks` now routes through `importMatchesTarget` * like every other match-side call site, so `importMatchesTarget` is this * function's only caller -- there is no longer a second computation to keep * in sync with. */ export declare function hasSingleFileImportSemantics(importerFile: string): boolean; /** * True when `importerFile`'s language is Python -- the only language * `matchesFile`'s Strategy 5 (`matchesPythonModule`) is a confirmed real * semantic for (#929). Unlike `hasSingleFileImportSemantics` above, this * isn't backed by a `LanguageDefinition` flag: `matchesPythonModule` is * Python-specific by construction (dotted-module parsing, `__init__.py` * handling), not a generic per-language toggle other languages could * legitimately opt into, so a direct language-identity check is the honest * representation. Shared by `importMatchesTarget`'s `allowPythonModuleMatching` * argument -- see `matchesFile`'s doc comment for the false-hub this guards * against. */ export declare function hasPythonModuleSemantics(importerFile: string): boolean; /** * True when `importerFile`'s language sets `LanguageDefinition.namespaceStyleImports` * (PHP today) -- see that flag's doc comment for the case-insensitive * PSR-4-vs-Rust distinction this drives (#1028). Unlike `hasPythonModuleSemantics` * above, this IS backed by a `LanguageDefinition` flag, mirroring * `hasSingleFileImportSemantics`: `matchesPHPNamespace`'s case-insensitive, * directory-mirroring semantic is a genuine per-language toggle another * language's own namespace convention could legitimately opt into later, * unlike Python's dotted-module parsing. Shared by `importMatchesTarget`'s * `allowNamespaceMatching` argument -- see `matchesFile`'s doc comment for * the false-hub (a Rust bare `use crate::{Error}` self-matching * `src/error.rs`) this guards against. */ export declare function hasNamespaceMatchingSemantics(importerFile: string): boolean; /** * Resolve a relative import specifier against its importer's file path. * * Acts on specifiers matching `RELATIVE_IMPORT_PATTERN`: `./`/`../`-prefixed, * or the bare `.`/`..` themselves (#935) — Node/TS module resolution treats a * bare `.` as "this directory" and a bare `..` as "the parent directory" * exactly like their slash-suffixed forms (`import { x } from '.'` in a * same-directory barrel re-export test is the confirmed real-world shape: a * genuine self-import that used to be stored as the literal, never-matching * string `"."`). Package specifiers (e.g. `@liendev/core`, `lodash`), dotted * Python-style *absolute* imports, and absolute paths pass through unchanged. * Since #904, Python's leading-dot *relative* imports (`.foo`, `..pkg`) DO * reach this function too — `PythonImportExtractor` converts them to this * same `./`/`../`-prefixed shape at extraction time (see * `ast/languages/python.ts`'s `convertPythonRelativeImport`) before * `resolveImportSpecifier` calls this, so the bare-dot case added here never * actually fires for Python; it exists for languages (JS/TS today) whose * extractor stores the raw source literal as-is. * * Returns the resolved path in the same form as `importerFile` — relative when * `importerFile` is relative, absolute when absolute. Any trailing slash is * stripped: a bare `./`/`.` or `../`/`..` specifier (Python's `from . import X` * / `from .. import X`, converted with an empty remainder — see * `convertPythonRelativeImport` — or JS/TS's own bare `'.'`/`'..'`) resolves * to the importer's own directory (or its parent) with nothing joined after * it, and `path.posix.normalize`/`join` leave that directory's trailing slash * intact, which would otherwise never boundary-match a target path (those * never carry one). The caller's downstream normalization (`normalizePath`) * is what ultimately strips extensions and the workspace-root prefix, so no * other work is needed here. * * @param importerFile - File path of the chunk doing the importing * @param specifier - The raw import specifier from source code * @returns Resolved path for relative specifiers; the original string otherwise */ export declare function resolveRelativeImport(importerFile: string, specifier: string): string; /** * Resolve a bare workspace package specifier (`@scope/pkg`, `pkg`) to that * package's workspace-relative source entry file, when `workspacePackages` * has an entry for it. See `resolveWorkspacePackageEntries` in * `../workspace-packages.ts` for how the map is built. * * Only exact bare-specifier matches resolve — deep imports into a package's * subpath (`@scope/pkg/subpath`) pass through unchanged (see that module's * doc comment for why this is the deliberate v1 scope). Specifiers with no * matching workspace package (external npm deps, or an empty/absent map for * non-monorepo projects) also pass through unchanged, so this is a no-op * everywhere it doesn't apply. * * @param specifier - The raw (or already relative-resolved) import specifier * @param workspacePackages - Map of package name -> workspace-relative entry file * @returns The resolved entry file path, or `specifier` unchanged */ export declare function resolveWorkspaceImport(specifier: string, workspacePackages: ReadonlyMap): string; /** * Gets a canonical path representation (relative to workspace, with extension). * * @param filepath - The file path to canonicalize * @param workspaceRoot - The workspace root directory (normalized with forward slashes) * @returns Canonical path */ export declare function getCanonicalPath(filepath: string, workspaceRoot: string): string; /** * Determines if a file is a test file based on naming conventions. * * Uses precise regex patterns to avoid false positives: * - Files with .test. or .spec. extensions (e.g., foo.test.ts, bar.spec.js) * - Files with _test. or _spec. suffixes (e.g., user_spec.rb, math_test.go) * - Files in test/, tests/, spec/, specs/, or __tests__/ directories * * Avoids false positives like: * - contest.ts (contains ".test." but isn't a test) * - latest/config.ts (contains "/test/" but isn't a test) * - mytest.ts (no `_` boundary before "test") * * The directory-segment check is case-insensitive (#925): a capitalized * `Tests/` directory is mainstream outside the JS/TS/Ruby/Go ecosystems that * motivated the original lowercase-only pattern -- symfony/console (and the * wider Symfony ecosystem) uses `Tests/` as its one and only test directory, * with no lowercase `tests/` anywhere in the repo, so a case-sensitive check * excluded literally every PHP test file in it from test-chunk scanning * before import-matching ever ran. This is safe to broaden for every * language: the check still requires an EXACT path segment (bounded by `/` * or the string start/end on both sides), so `Latest/`, `Contest/`, and * `Testing/` still correctly fail regardless of case -- only a segment that * IS exactly `test`/`tests`/`spec`/`specs`/`__tests__` (any casing) matches. * * Swift uses different conventions (XCTest `FooTests.swift` files and a * Swift Package Manager `Tests/` directory). Those checks are scoped to * `.swift` paths so behavior for other languages is unchanged. * * .NET/xUnit/NUnit/MSTest use a `Tests` suffix glued onto a longer * identifier rather than a delimited `test`/`spec` segment: project * directories like `UnitTests/`, `IntegrationTests/`, `AutoMapper.DI.Tests/` * and files like `ScopeTests.cs`, `ConfigurationFeatureTest.cs`. Those checks * are scoped to `.cs` paths and are case-sensitive (`Tests`/`Test`, capital * T) so `Latest.cs`/`Contest.cs` and a `latest/`-style directory are not * misclassified, and no other language's behavior moves. * * @param filepath - The file path to check * @returns True if the file is a test file */ export declare function isTestFile(filepath: string): boolean; //# sourceMappingURL=path-matching.d.ts.map