On this page
Dependency validation checks for monorepo workspaces: unused deps, undeclared deps, dead modules, and circular dependency detection.
#Dependency validation
rlsbl validates dependencies at two levels: workspace-level (between monorepo packages) and file-level (within a single package). Four workspace checks detect mismatches between declared and actual dependencies across all 4 supported language ecosystems. Three intra-package checks detect dead code and import cycles using per-file BFS traversal and Tarjan's algorithm for strongly connected components.
#Workspace dependency checks
These four checks compare declared dependencies in workspace manifests against actual import usage discovered by scanning source code. They share a common import scan cache (ctx._dep_import_cache) to avoid redundant source tree walks, and all four run during rlsbl check --tag workspace.
#deps-unused
Detects workspace dependencies declared in the project manifest that are not actually imported anywhere in the project's source code. An unused dependency adds unnecessary coupling and bloats the install footprint for consumers.
- Scans both lib and test code for imports matching workspace sibling names
- A dep is unused if zero source files import it (regardless of context)
- Supports overrides via
.rlsbl-monorepo/dep-overrides.tomlwith mandatoryreasonfield - Override format:
[[unused_allowed]]entries withpackage,dep, andreasonkeys - Imports inside
try/except ImportErrorblocks only count as usage for optional deps (scopedev/peer). A hard dependency (scoperuntime/explicit) that is only imported inside a guard is flagged -- declaring a hard dependency but importing it conditionally is contradictory (declare it optional or import it unconditionally). - If the same dep is declared with multiple scopes, hard scopes take precedence: declared hard anywhere means treated as hard.
#deps-undeclared
Detects imports of workspace sibling packages that are not declared as dependencies in the project's manifest. Undeclared dependencies work locally because all packages share a repository, but break when the project is installed standalone.
- Only checks production code (non-test context) -- test files have more lenient rules
- Self-imports are excluded (a package importing its own submodules is fine)
- Detected import must match a workspace sibling name to trigger
#deps-runtime-test-only
Detects runtime dependencies that are only imported in test code and never in production source files. These should be declared as dev dependencies instead, since shipping them as runtime dependencies forces consumers to install packages they will never use.
- Checks deps with
scope="runtime"in the workspace manifest - Flags any runtime dep that appears in
test_importsbut not inlib_imports - Helps maintain correct dependency scoping for published packages
#deps-dev-in-lib
Detects dev dependencies that are imported in production code, indicating they should be declared as runtime dependencies instead. When a dev dependency is used in library or application code, consumers who install the package will get import errors because dev dependencies are not installed transitively.
- Checks deps with
scope="dev"in the workspace manifest - Flags any dev dep that appears in
lib_imports - Catches incorrect scoping that would break consumers at install time
#Intra-package checks
These checks build per-file import graphs within a single project to detect structural problems like unreachable code and circular dependencies. They operate at file granularity rather than package granularity and do not share the workspace import cache.
#dead-modules
Detects source files that are unreachable from any entry point via BFS on the file-level import graph. Dead modules are production code that can never be executed because no import chain connects them to the package's public API or executable entry points.
Algorithm:
- Identify entry points (language-specific, see table below)
- Build file-level import graph (each file maps to the set of files it imports)
- BFS from all entry points
- Report files in production code that are never reached
Exclusions (6 categories, common to all languages):
- Test files (matching 7 patterns from
_NON_PRODUCTION_PATTERNS) .selfdocdirectories_builddirectories- Browser asset directories (
static,public,assets-- 3 directory names) - Generated files (
.g.dartfor Dart)
Per-language behavior:
| Language | Detection function | Entry points | Scope |
|---|---|---|---|
| Python | find_dead_modules() | __init__.py files + cross-reference via import prefix matching | All production .py files |
| Go | find_dead_go_packages() | Packages imported by any non-test file outside the package | Only internal/ packages (Go enforces visibility elsewhere) |
| npm | find_dead_npm_modules() | package.json exports/main/bin fields, resolved to source | All production .js/.ts/.mjs/.cjs/.tsx files |
| Dart | find_dead_dart_modules() | lib/<name>.dart barrel file + bin/*.dart scripts | All production .dart files |
#circular-deps
Detects circular dependencies by computing strongly connected components using Tarjan's algorithm on the file-level import graph. Only cycles involving 2 or more distinct nodes are reported as violations; self-loops (a file importing itself) are not flagged since they are harmless in all supported languages.
Severity by language:
| Language | Severity | Rationale |
|---|---|---|
| npm (JS/TS) | error (fail) | Circular imports cause runtime issues (undefined values, initialization order bugs) |
| Python | warning | Python handles circular imports at runtime but they indicate design problems |
| Dart | warning | Dart handles cycles but they indicate poor layering |
| Go | excluded | The Go compiler rejects circular imports natively; rlsbl does not duplicate that check |
#library-lint
Enforces quality constraints specific to library packages published for consumption by other projects. This check detects imports, I/O patterns, and platform-specific APIs that are inappropriate for reusable library code and would cause problems for downstream consumers:
- Detects imports inappropriate for library code (e.g.,
dart:ioin a pure Dart library) - Detects stdout/stderr writes in library code (libraries should not print directly)
- Only applies to projects marked
library = truein workspace.toml
#Dead workspace packages
The dead-workspace-packages check operates at the workspace level, identifying library packages that no workspace sibling imports. A library with zero importers may indicate abandoned code, an incomplete migration, or a package that is only consumed by external projects outside the monorepo.
Criteria:
- Only checks projects with
library = true(apps/CLIs are entry points, not consumed) - Skips
dev_node = trueprojects - Checks both lib and test import contexts from all other workspace projects
Results:
| Condition | Severity | Message |
|---|---|---|
| Library imported in production code by at least one sibling | pass (alive) | -- |
| Library only imported in test code | warn | "library 'X' is only imported in test code by workspace siblings (A, B)" |
| Library not imported by any workspace package | warn | "library 'X' is not imported by any workspace package" |
Published libraries may still be consumed externally, so zero workspace importers is a warning, not an error.
#Language support matrix
The following matrix shows which dependency validation checks are supported for each of the 4 languages with import scanning (Python, Go, npm/JS/TS, and Dart). Not all checks apply to every language -- for example, Go's compiler natively rejects circular imports, so rlsbl skips that check for Go projects.
[selfdoc: custom directive 'table-feature-matrix' failed: No module named 'rlsbl']
#Configuration
#dep-overrides.toml
Located at .rlsbl-monorepo/dep-overrides.toml, this configuration file allows suppressing deps-unused errors for dependencies that are intentionally declared but not statically detectable by import scanning (e.g., dynamic imports, plugin loading, or runtime reflection).
[[unused_allowed]]
package = "my-app"
dep = "my-lib"
reason = "Used via dynamic import at runtime, not statically detectable"Every entry requires all three fields (package, dep, reason). Empty reason values are rejected. This ensures the override has a documented justification.
#batch_limits in config.json
Controls batch size validation for changelog entries, limiting how many commits a single entry can reference and how many entries can reference the same commit. While not directly related to dependency validation, these limits are part of the same check infrastructure and run alongside dep checks during rlsbl check:
| Key | Type | Default | Description |
|---|---|---|---|
max_commits_per_entry | int | 5 | Maximum commit hashes allowed in a single JSONL entry |
max_entries_per_commit | int | 5 | Maximum JSONL entries that may reference the same commit |
exclusions | array | [] | Per-violation silencers, each with mandatory reason plus commits or entries |
#Source modules
The dependency validation system is implemented in two core modules: dep_validation handles workspace-level checks (unused, undeclared, test-only, dev-in-lib, dead packages) while import_scanners provides the per-language AST-based import extraction that feeds all dependency analysis.
#rlsbl.dep_validation
Dependency validation for monorepo workspaces.
Checks for unused declared dependencies and undeclared imports across workspace projects. Uses import scanners to compare actual source imports against manifest-declared dependencies.
#load_dep_overrides
def load_dep_overrides(root: str) -> dict[tuple[str, str], str]Load dep-overrides.toml from the monorepo config directory.
Returns a dict mapping (package, dep) to reason string. Raises ValueError if an entry is missing a required 'reason' field. Returns empty dict if the file does not exist.
#_get_imported_workspace_packages
def _get_imported_workspace_packages(project_dir: str, workspace_names: set[str], exclude_dirs: list[str] | None=None, *, module_path_map: dict[str, str] | None=None, namespace_map: dict[str, str] | None=None, import_names: dict[str, str] | None=None, jvm_package_map: dict[str, str] | None=None) -> tuple[set[str], set[str], set[str]]Scan a project for workspace imports, split by context.
Args:
project_dir: absolute path to the project root.workspace_names: set of all workspace member package names.exclude_dirs: directory paths to skip during the walk.module_path_map: mapping of workspace project name to its Go
module path (from go.mod). Passed through to GoImportScanner.
namespace_map: mapping of namespace-qualified import paths to
workspace project names. Passed through to PythonImportScanner.
import_names: mapping of project_name -> import_name from workspace
config. Passed through to PythonImportScanner.
jvm_package_map: mapping of dotted package prefix to workspace
project name. Passed through to JavaImportScanner and KotlinImportScanner.
Returns (lib_imports, test_imports, guarded_imports) where each is a set of workspace package names. Guarded imports are those inside try/except ImportError blocks -- they count as "used" (not unused) but should not trigger undeclared-dep errors.
#check_unused_deps
def check_unused_deps(project_name: str, project_dir: str, manifest_deps_with_scope: dict[str, str], workspace_names: set[str], whitelist: dict[tuple[str, str], str], *, _cached_imports: tuple[set[str], set[str], set[str]] | None=None) -> list[str]Check for declared workspace deps that no source file imports.
Args:
project_name: name of the project being checked.project_dir: absolute path to the project directory.manifest_deps_with_scope: mapping of declared intra-workspace
dependency name -> scope ("runtime", "dev", "peer", "explicit").
workspace_names: set of all workspace member package names.whitelist: mapping of (package, dep) -> reason for allowed unused deps._cached_imports: optional pre-computed (lib_imports, test_imports,
guarded_imports) tuple to avoid redundant scans when multiple checks share the same project.
Returns:
- list of error strings (empty means all good).
#check_undeclared_deps
def check_undeclared_deps(project_name: str, project_dir: str, manifest_deps: set[str], workspace_names: set[str], *, _cached_imports: tuple[set[str], set[str], set[str]] | None=None) -> list[str]Check for imports from workspace packages not declared as deps.
Only checks lib/ imports (non-test context) against declared dependencies. Test files have more lenient rules and are skipped. Guarded imports (try/except ImportError) are excluded -- optional imports don't need to be declared as dependencies.
Args:
project_name: name of the project being checked.project_dir: absolute path to the project directory.manifest_deps: set of declared intra-workspace dependency names.workspace_names: set of all workspace member package names._cached_imports: optional pre-computed (lib_imports, test_imports,
guarded_imports) tuple to avoid redundant scans when multiple checks share the same project.
Returns:
- list of error strings (empty means all good).
#check_runtime_test_only
def check_runtime_test_only(manifest_deps_with_scope: dict[str, str], lib_imports: set[str], test_imports: set[str]) -> list[str]Find runtime deps that are only used in test code.
For each dependency where scope="runtime": if it appears in test_imports but NOT in lib_imports, it is flagged.
Args:
manifest_deps_with_scope: mapping of dep name -> scope string.lib_imports: workspace package names found in production code.test_imports: workspace package names found in test code.
Returns:
- list of flagged dependency names.
#check_dev_in_lib
def check_dev_in_lib(manifest_deps_with_scope: dict[str, str], lib_imports: set[str]) -> list[str]Find dev deps that are imported in production code.
For each dependency where scope="dev": if it appears in lib_imports, it is flagged.
Args:
manifest_deps_with_scope: mapping of dep name -> scope string.lib_imports: workspace package names found in production code.
Returns:
- list of flagged dependency names.
#_is_non_production_path
def _is_non_production_path(filepath: str, project_dir: str) -> boolCheck if a file path is in a non-production context.
Delegates to _is_test_context from import_scanners.py to detect test directories, example directories, and test file patterns.
#_python_module_name
def _python_module_name(filepath: str, project_dir: str) -> str | NoneDerive the dotted module name from a Python file path.
Returns None for files that cannot be mapped to a module name (e.g. files outside a package structure).
#_collect_python_imports
def _collect_python_imports(filepath: str, project_dir: str) -> set[str]Collect all import targets from a Python file.
Returns a set of dotted module names that are imported. Handles absolute imports (import foo, from foo.bar import baz) and relative imports (from .utils import helper, from ..core import x). Relative imports are resolved to absolute dotted paths using the file's position within the project directory.
#_collect_init_exports
def _collect_init_exports(filepath: str) -> set[str]Collect names exported from an __init__.py file.
Looks for __all__ definitions and import statements. Returns module names that are imported by the __init__.py.
#find_dead_modules
def find_dead_modules(project_dir: str, exclude_dirs: list[str] | None=None) -> list[str]Find Python modules not referenced by any other module in the project.
A module is considered dead if:
- No other module in the project imports it (by any prefix match)
- It is not listed in any __init__.py's __all__ or imported by
any __init__.py
Only checks Python projects. Non-production files (tests, examples) are excluded from the scan.
Args:
project_dir: absolute path to the project root.exclude_dirs: directory paths to skip during the walk
(relative to project_dir or absolute).
Returns:
- list of relative paths of dead modules (e.g. ["mylib/unused.py"]).
#_go_package_dir
def _go_package_dir(filepath: str) -> strReturn the directory of a Go file (its package directory).
#find_dead_go_packages
def find_dead_go_packages(project_dir: str, exclude_dirs: list[str] | None=None) -> list[str]Find Go internal packages not referenced by any non-test code.
A Go internal package is dead if no non-test .go file outside that package imports it. Only packages under internal/ subdirectories are checked, since those are the packages with restricted visibility in Go's module system.
Args:
project_dir: absolute path to the Go project root (where go.mod lives).exclude_dirs: directory paths to skip during the walk
(relative to project_dir or absolute).
Returns:
- list of relative paths of dead internal packages
- (e.g. ["internal/unused"]).
#_resolve_npm_file
def _resolve_npm_file(path: str) -> str | NoneResolve a single path to an existing file using npm conventions.
Tries the exact path, then with each extension appended, then as a directory with index files. Also handles .js -> .ts mapping for TypeScript projects.
Returns the absolute file path if found, None otherwise.
#_collect_export_paths
def _collect_export_paths(value: object) -> list[str]Recursively collect all file path strings from a package.json exports value.
The exports field can be:
- A string: "./dist/index.js"
- A dict with condition keys: {"import": "./dist/index.mjs", "require": "./dist/index.cjs"}
- A nested subpath map: {".": {"import": "..."}, "./sub": "..."}
- A list (rarely): ["./a.js", "./b.js"]
Collects all string values that look like file paths (start with ".").
#_resolve_npm_entry_points
def _resolve_npm_entry_points(project_dir: str) -> set[str]Extract and resolve entry point file paths from package.json.
Parses exports, main, and bin fields. Resolves each declared path to an absolute filesystem path, handling .js -> .ts mapping and directory -> index file resolution.
Returns a set of absolute file paths. Missing files are skipped.
#_build_npm_import_graph
def _build_npm_import_graph(project_dir: str, exclude_dirs: list[str] | None=None) -> dict[str, set[str]]Build a file-level import graph for an npm project.
Uses NpmAstLinter.scan_imports() to collect all imports, then resolves relative imports to absolute file paths.
Returns a dict mapping each source file's absolute path to a set of absolute paths it imports (only resolved relative imports).
#_is_inside_python_package
def _is_inside_python_package(filepath: str, project_dir: str) -> boolCheck if a file is inside a directory containing __init__.py.
Walks up from the file's directory looking for __init__.py, stopping at project_dir. If any directory from the file's parent up to (but not above) project_dir contains __init__.py, the file is considered a Python data resource, not an npm module.
#find_dead_npm_modules
def find_dead_npm_modules(project_dir: str, exclude_dirs: list[str] | None=None) -> list[str]Find npm source files unreachable from any package.json entry point.
A source file is "dead" if there is no path through the import graph from any declared entry point (exports, main, bin) to that file.
Args:
project_dir: absolute path to the npm project root.exclude_dirs: directory paths to skip during the walk
(relative to project_dir or absolute).
Returns:
- sorted list of relative paths of dead source files.
#_read_dart_package_name
def _read_dart_package_name(project_dir: str) -> str | NoneRead the package name from pubspec.yaml.
Returns None if pubspec.yaml does not exist or has no 'name' field.
#_resolve_dart_entry_points
def _resolve_dart_entry_points(project_dir: str) -> set[str]Determine Dart entry point files for reachability analysis.
Entry points are:
- lib/
.dart (barrel file / main library entry) - bin/*.dart (executable scripts)
Returns a set of absolute file paths.
#_resolve_dart_import
def _resolve_dart_import(specifier: str, importing_file: str, project_dir: str, package_name: str | None) -> str | NoneResolve a Dart import specifier to an absolute file path.
Handles:
- Relative imports: 'src/foo.dart', '../utils.dart'
- Self-package imports: 'package:mylib/src/foo.dart' -> lib/src/foo.dart
Returns absolute path if resolved, None otherwise. Skips dart: imports and external package: imports.
#_build_dart_import_graph
def _build_dart_import_graph(project_dir: str, exclude_dirs: list[str] | None=None) -> dict[str, set[str]]Build a file-level import graph for a Dart project.
Walks all .dart files, extracts import/export statements via regex, and resolves relative and self-package imports to absolute paths.
Returns a dict mapping each source file's absolute path to a set of absolute paths it imports/exports (only resolved intra-package refs).
#find_dead_dart_modules
def find_dead_dart_modules(project_dir: str, exclude_dirs: list[str] | None=None) -> list[str]Find Dart source files unreachable from any entry point.
A .dart file is "dead" if there is no path through the import/export graph from any entry point (barrel file, bin scripts) to that file.
Test files (test/, *_test.dart) are excluded from the scan.
Args:
project_dir: absolute path to the Dart project root.exclude_dirs: directory paths to skip during the walk
(relative to project_dir or absolute).
Returns:
- sorted list of relative paths of dead source files.
#DeadWorkspacePackage
A workspace package with no workspace importers.
#find_dead_workspace_packages
def find_dead_workspace_packages(projects: list[dict], import_cache: dict[str, tuple[set[str], set[str]]]) -> list[DeadWorkspacePackage]Find library packages that no other workspace package imports.
A library package is "dead" at the workspace level if its name does not appear in any other project's lib_imports or test_imports sets.
Args:
projects: list of workspace project dicts (must have "name",
and optionally "library" and "dev_node" keys).
import_cache: mapping of project name to (lib_imports, test_imports,
guarded_imports) as produced by _build_dep_import_cache in rlsbl/checks/_common.py.
Returns:
- list of DeadWorkspacePackage for packages with no workspace importers.
#find_circular_deps
def find_circular_deps(import_graph: dict[str, set[str]]) -> list[list[str]]Find circular dependencies in a file-level import graph using Tarjan's SCC.
Args:
import_graph: mapping of file path to the set of file paths it imports.
Returns:
- list of cycles, where each cycle is a list of file paths forming
- the strongly connected component. Only SCCs with 2+ nodes are
- returned (self-loops are not interesting).
#_build_python_import_graph
def _build_python_import_graph(project_dir: str, exclude_dirs: list[str] | None=None) -> dict[str, set[str]]Build a file-level import graph for a Python project.
Uses _collect_python_imports to get dotted module names, then resolves them to file paths via a module-name-to-file mapping.
Returns a dict mapping each source file's relative path to a set of relative paths it imports (only intra-project imports that resolve to actual files).
#find_circular_python_deps
def find_circular_python_deps(project_dir: str, exclude_dirs: list[str] | None=None) -> list[list[str]]Find circular dependencies in a Python project.
Builds a file-level import graph from Python source files and runs Tarjan's SCC algorithm to detect cycles.
Args:
project_dir: absolute path to the project root.exclude_dirs: directory paths to skip during the walk.
Returns:
- list of cycles, each a sorted list of relative file paths.
#find_circular_npm_deps
def find_circular_npm_deps(project_dir: str, exclude_dirs: list[str] | None=None) -> list[list[str]]Find circular dependencies in an npm project.
Reuses _build_npm_import_graph() and runs Tarjan's SCC algorithm.
Args:
project_dir: absolute path to the project root.exclude_dirs: directory paths to skip during the walk.
Returns:
- list of cycles, each a sorted list of relative file paths.
#find_circular_dart_deps
def find_circular_dart_deps(project_dir: str, exclude_dirs: list[str] | None=None) -> list[list[str]]Find circular dependencies in a Dart project.
Builds a file-level import graph from Dart source files using regex to extract relative imports, then runs Tarjan's SCC algorithm.
Args:
project_dir: absolute path to the project root.exclude_dirs: directory paths to skip during the walk.
Returns:
- list of cycles, each a sorted list of relative file paths.
#rlsbl.import_scanners
Python, Dart, npm, Go, Java, and Kotlin import scanners for dependency-import validation.
Filters raw import data to workspace-relevant imports, handles language-specific edge cases, and distinguishes lib/ vs test/ contexts.
#ImportInfo
A single workspace-relevant import detected in a source file.
#_is_test_context
def _is_test_context(filepath: str, project_path: str) -> boolDetermine whether a file is in a non-production context.
Uses a layered approach to avoid false positives for production paths that happen to contain directory names like "test":
Layer 1 -- Unconditional directories (match at any depth): __tests__/, testdata/
Layer 2 -- Root-relative directories (match only as first component): test/, tests/, example/, examples/, integration_test/
Layer 3 -- File name patterns (checked against basename): test_.py, _test.py, _test.go, _test.dart, .test.[jt]sx?, .spec.[jt]sx?, conftest.py
#build_namespace_map
def build_namespace_map(projects, workspace_root: str) -> dict[str, str]Map namespace-qualified import paths to workspace project names.
For a project named 'protocols' at 'protocols/src/orxt/protocols/', returns {'orxt.protocols': 'protocols'}.
Algorithm:
- For each project, call detect_python_package_root() to get the
package root (e.g., 'src/orxt')
- The namespace is the package root's leaf directory name (e.g., 'orxt')
- Walk subdirectories of the package root looking for the project's
directory name
- If src/orxt/protocols/ exists and project name is 'protocols',
map 'orxt.protocols' -> 'protocols'
#PythonImportScanner
Scan Python source files for workspace-relevant imports.
Uses the AST-based scanner from the lint system, then post-processes to filter out stdlib, relative imports, and non-workspace packages. Supports namespace package detection via namespace_map and import_names.
#scan
def scan(self, project_path: str, workspace_names: set[str], exclude_dirs: list[str] | None=None, *, namespace_map: dict[str, str] | None=None, import_names: dict[str, str] | None=None) -> list[ImportInfo]Scan project_path for Python imports matching workspace members.
Args:
project_path: absolute path to the project root.workspace_names: set of workspace member package names
(as they appear in pyproject.toml, e.g. "my-lib").
exclude_dirs: directory paths to skip during the walk
(relative to project_path or absolute).
namespace_map: mapping of namespace-qualified import paths
to workspace project names (e.g., {'orxt.protocols': 'protocols'}). Built by build_namespace_map().
import_names: mapping of project_name -> import_name from workspace
config. Used for explicit import_name overrides.
Returns:
- list of ImportInfo for imports that match workspace members.
#DartImportScanner
Scan Dart source files for workspace-relevant package imports.
Uses regex to extract package names from import/export statements. Checks for missing generated (.g.dart) files when build_runner is configured.
#scan
def scan(self, project_path: str, workspace_names: set[str], exclude_dirs: list[str] | None=None) -> list[ImportInfo]Scan project_path for Dart imports matching workspace members.
Args:
project_path: absolute path to the project root.workspace_names: set of workspace member package names
(as they appear in pubspec.yaml).
exclude_dirs: directory paths to skip during the walk
(relative to project_path or absolute).
Returns:
- list of ImportInfo for imports that match workspace members.
Raises:
RuntimeError: if build.yaml exists but no .g.dart files
are found in the project (missing code generation).
#_check_generated_files
def _check_generated_files(self, project_path: str) -> NoneRaise RuntimeError if build_runner is configured but no .g.dart files exist.
#_extract_npm_bare_name
def _extract_npm_bare_name(specifier: str) -> str | NoneExtract bare package name from an npm import specifier.
Returns None for relative imports, Node.js builtins, and node:-prefixed builtins. For scoped packages (@scope/pkg/foo), returns @scope/pkg. For unscoped (pkg/foo), returns pkg.
#NpmImportScanner
Scan JS/TS source files for workspace-relevant imports.
Uses the AST-based scanner from the npm lint system, then post-processes to filter out relative imports, Node.js builtins, and non-workspace packages.
#scan
def scan(self, project_path: str, workspace_names: set[str], exclude_dirs: list[str] | None=None) -> list[ImportInfo]Scan project_path for JS/TS imports matching workspace members.
Args:
project_path: absolute path to the project root.workspace_names: set of workspace member package names
(as they appear in package.json, e.g. "@scope/my-lib").
exclude_dirs: directory paths to skip during the walk
(relative to project_path or absolute).
Returns:
- list of ImportInfo for imports that match workspace members.
#GoImportScanner
Scan Go source files for workspace-relevant imports.
Uses the tree-sitter-based scanner from the Go lint system, then post-processes to filter to imports matching other workspace projects' Go module paths.
#scan
def scan(self, project_path: str, workspace_names: set[str], exclude_dirs: list[str] | None=None, *, module_path_map: dict[str, str] | None=None) -> list[ImportInfo]Scan project_path for Go imports matching workspace members.
Args:
project_path: absolute path to the project root.workspace_names: set of workspace member package names.exclude_dirs: directory paths to skip during the walk
(relative to project_path or absolute).
module_path_map: mapping of workspace project name to its
Go module path (from go.mod). Only Go projects appear in this map. Required for Go import detection.
Returns:
- list of ImportInfo for imports that match workspace members.
#_match_workspace_import
def _match_workspace_import(import_path: str, module_to_name: dict[str, str]) -> str | NoneCheck if an import path belongs to a workspace sibling.
An import matches a workspace module if the import path equals the module path or starts with it followed by '/'.
#build_jvm_package_map
def build_jvm_package_map(projects: list, workspace_root: str) -> dict[str, str]Map Java/Kotlin package prefixes to workspace project names.
For each workspace project with a pom.xml or build.gradle(.kts), reads the groupId (from POM) or group (from Gradle) and maps it to the project name. This allows import scanning to determine which workspace project an import like com.example.foo.Bar belongs to.
Args:
projects: list of workspace project dicts/objects withname
and path attributes.
workspace_root: absolute path to the workspace root.
Returns:
- dict mapping dotted package prefix to workspace project name.
- E.g.
{"com.example.foo": "foo-lib"}
#_JvmImportScannerBase
Base class for Java and Kotlin import scanners.
Scans source files for import statements matching workspace projects via a package prefix map. Subclasses specify which file extensions to scan.
#scan
def scan(self, project_path: str, workspace_names: set[str], exclude_dirs: list[str] | None=None, *, package_map: dict[str, str] | None=None) -> list[ImportInfo]Scan project_path for JVM imports matching workspace members.
Args:
project_path: absolute path to the project root.workspace_names: set of workspace member package names.exclude_dirs: directory paths to skip during the walk
(relative to project_path or absolute).
package_map: mapping of dotted package prefix to workspace
project name. Built by build_jvm_package_map(). Required for JVM import detection.
Returns:
- list of ImportInfo for imports that match workspace members.
#JavaImportScanner
Scan Java source files for workspace-relevant imports.
Uses regex to extract import statements from .java files, then matches against the workspace package prefix map.
#KotlinImportScanner
Scan Kotlin source files for workspace-relevant imports.
Uses regex to extract import statements from .kt and .kts files, then matches against the workspace package prefix map.