eslint-plugin-sonarjs
Version:
325 lines (324 loc) • 14.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.moduleTypeCache = exports.dependenciesCache = void 0;
exports.getDependencies = getDependencies;
exports.getModuleType = getModuleType;
exports.setCurrentFileInlineDependencies = setCurrentFileInlineDependencies;
exports.withCurrentFileInlineDependencies = withCurrentFileInlineDependencies;
exports.getDependenciesSanitizePaths = getDependenciesSanitizePaths;
exports.isAngularProject = isAngularProject;
exports.getReactVersion = getReactVersion;
exports.parseReactVersion = parseReactVersion;
exports.getVueVersion = getVueVersion;
exports.getTypeScriptSignalsFromPackageJsonFiles = getTypeScriptSignalsFromPackageJsonFiles;
exports.getNodeVersionSignal = getNodeVersionSignal;
exports.getTypeScriptVersionSignal = getTypeScriptVersionSignal;
const cache_js_1 = require("../cache.js");
const node_fs_1 = __importDefault(require("node:fs"));
const posix_1 = require("node:path/posix");
const semver_1 = require("semver");
const files_js_1 = require("../files.js");
const closest_js_1 = require("./closest.js");
const all_in_parent_dirs_js_1 = require("./all-in-parent-dirs.js");
const top_dir_js_1 = require("./top-dir.js");
const types_js_1 = require("./resolvers/types.js");
const parsed_dependency_files_js_1 = require("./parsed-dependency-files.js");
const MODULE_TYPE_BY_EXTENSION = {
'.mjs': 'module',
'.mts': 'module',
'.cjs': 'commonjs',
'.cts': 'commonjs',
};
/**
* Cache for the available dependencies by closest manifest directory and search boundary.
* Exported for tests.
*/
class DependenciesCache {
constructor() {
this.cache = new Map();
}
get(dir, topDir) {
let entriesByTopDir = this.cache.get(dir);
if (!entriesByTopDir) {
entriesByTopDir = new Map();
this.cache.set(dir, entriesByTopDir);
}
let dependencies = entriesByTopDir.get(topDir);
if (!dependencies) {
dependencies = computeDependencies(dir, topDir);
entriesByTopDir.set(topDir, dependencies);
}
return dependencies;
}
has(dir, topDir) {
return this.cache.get(dir)?.has(topDir) ?? false;
}
clear() {
this.cache.clear();
}
get size() {
let size = 0;
for (const entriesByTopDir of this.cache.values()) {
size += entriesByTopDir.size;
}
return size;
}
}
exports.dependenciesCache = new DependenciesCache();
function computeDependencies(dir, topDir) {
const result = new Map();
for (const { dependencies } of (0, all_in_parent_dirs_js_1.getDependencyManifests)(dir, topDir, node_fs_1.default)) {
for (const [name, version] of dependencies) {
if (!result.has(name)) {
result.set(name, version);
}
}
}
return result;
}
/**
* Cache for module type signal by dirname. Exported for tests.
*/
exports.moduleTypeCache = new cache_js_1.ComputedCache((dir, topDir) => {
const closestDependencyManifestDirName = (0, closest_js_1.getClosestDependencyManifestDir)(dir, topDir);
if (!closestDependencyManifestDirName) {
return undefined;
}
const [firstManifest] = (0, all_in_parent_dirs_js_1.getDependencyManifests)(closestDependencyManifestDirName, topDir, node_fs_1.default);
return firstManifest?.moduleType;
});
/**
* Retrieve the dependencies of all the dependency manifest files available for the given file.
*
* @param dir dirname of the context.filename
* @param topDir working dir, will search up to that root
* @returns Map of dependency names to their version strings
*/
function getDependencies(dir, topDir) {
const closestDependencyManifestDir = (0, closest_js_1.getClosestDependencyManifestDir)(dir, topDir);
if (closestDependencyManifestDir) {
return exports.dependenciesCache.get(closestDependencyManifestDir, topDir);
}
return new Map();
}
/**
* Retrieve the module type signal for a file.
*
* Extension-specific module kinds (.mjs/.mts and .cjs/.cts) are explicit and
* take precedence. Otherwise, package.json#type from the closest manifest only
* is used. If that closest manifest exists but omits "type", default to CommonJS.
*/
function getModuleType(filePath, topDir) {
const extensionSignal = MODULE_TYPE_BY_EXTENSION[(0, posix_1.extname)(filePath).toLowerCase()];
if (extensionSignal) {
return extensionSignal;
}
return exports.moduleTypeCache.get((0, files_js_1.dirnamePath)(filePath), topDir);
}
/**
* Inline npm: imports parsed from the source file currently being linted (Deno-style).
* Set by the linter before each file is verified, cleared afterwards via clearFileCaches().
* Allows dependency-helper consumers (getReactVersion, getDependenciesSanitizePaths)
* to see the same dependency picture as rule-activation filtering.
*/
let currentFileInlineDependencies = null;
function setCurrentFileInlineDependencies(deps) {
currentFileInlineDependencies = deps;
}
/**
* Merges inline npm: imports with manifest dependencies, giving precedence to inline imports.
*/
function withCurrentFileInlineDependencies(manifest) {
if (!currentFileInlineDependencies || currentFileInlineDependencies.size === 0) {
return manifest;
}
const merged = new Map(manifest);
for (const [name, inlineVersion] of currentFileInlineDependencies) {
// Inline npm: imports are the version actually loaded at runtime for this file,
// so they take precedence over the project-wide manifest version.
merged.set(name, inlineVersion);
}
return merged;
}
function getDependenciesSanitizePaths(context) {
const filePath = (0, files_js_1.normalizeToAbsolutePath)(context.filename);
const topDir = (0, top_dir_js_1.getDependencyTopDir)(context, filePath);
return withCurrentFileInlineDependencies(getDependencies((0, files_js_1.dirnamePath)(filePath), topDir));
}
/**
* Determines whether a file belongs to an Angular project, based on the presence of
* `@angular/core` in the dependency manifests visible to the file: the closest `package.json` and
* every ancestor manifest up to `topDir`, unioned (see {@link getDependencies}). A root manifest
* that declares `@angular/core` therefore also marks nested packages as Angular — intentional, so
* the common monorepo layout where Angular is declared only at the workspace root (e.g. Nx) is
* still detected. The trade-off is that a nested non-Angular package inheriting a root Angular
* dependency is also treated as Angular; this only matters for paths already matching the narrow
* `environments/environment.<env>.ts` shape, so the practical over-reach is negligible.
*
* This is a path-based predicate (no `Rule.RuleContext`), so it can be used both by the
* scope-classification heuristic in `common/filter/filter-path.ts` — which decides MAIN/TEST
* before any rule runs — and, if needed, by rules. Keeping the Angular signal in one shared
* helper avoids duplicating the dependency list, and the underlying lookup is cached.
*
* @param filePath normalized absolute path of the file
* @param topDir normalized absolute directory bounding the upward manifest search (project base dir)
* @returns true when `@angular/core` is declared in the file's closest manifest or any ancestor up to `topDir`
*/
function isAngularProject(filePath, topDir) {
return getDependencies((0, files_js_1.dirnamePath)(filePath), topDir).has('@angular/core');
}
/**
* Gets the React version from the closest package.json.
*
* @param context ESLint rule context
* @returns React version string (coerced from range) or null if not found
*/
function getReactVersion(context) {
const filePath = (0, files_js_1.normalizeToAbsolutePath)(context.filename);
const dependencies = withCurrentFileInlineDependencies(getDependencies((0, files_js_1.dirnamePath)(filePath), (0, top_dir_js_1.getDependencyTopDir)(context, filePath)));
const reactVersion = dependencies.get('react');
// Deno npm: imports reflect the actual runtime version and are intentionally included here, unlike the TypeScript/Node.js version signals
if (!reactVersion) {
return null;
}
return parseReactVersion(reactVersion);
}
/**
* Parses a React version string and returns a valid semver version.
* Exported for testing purposes.
*
* @param reactVersion Version string from package.json (e.g., "^18.0.0", "19.0", "catalog:frontend")
* @returns Valid semver version string or null if parsing fails
*/
function parseReactVersion(reactVersion) {
return coerceDependencyVersion(reactVersion);
}
/**
* Gets the raw Vue dependency range from the closest package.json.
*
* Unlike getReactVersion, this returns the range as declared (e.g. "^2.7.0 || ^3.0.0")
* rather than a single coerced version, since S8961 needs to know whether Vue 3
* is a possibility for the range, not just its lower bound.
*
* @param context ESLint rule context
* @returns Vue dependency range string, or null if not found
*/
function getVueVersion(context) {
const filePath = (0, files_js_1.normalizeToAbsolutePath)(context.filename);
const dependencies = withCurrentFileInlineDependencies(getDependencies((0, files_js_1.dirnamePath)(filePath), (0, top_dir_js_1.getDependencyTopDir)(context, filePath)));
return dependencies.get('vue') ?? null;
}
/**
* Coerces a dependency version string or range (e.g., "^18.0.0", "19.0") to a
* valid semver version. Returns null for non-semver strings such as pnpm
* catalog references (e.g., "catalog:frontend").
*/
function coerceDependencyVersion(version) {
try {
return (0, semver_1.minVersion)(version)?.version ?? null;
}
catch {
return null;
}
}
function getAllDependencySignals(packageJson) {
return {
...packageJson.dependencies,
...packageJson.devDependencies,
...packageJson.peerDependencies,
...packageJson.optionalDependencies,
};
}
function getDependencyVersionSignal(packageJson, dependencyName) {
const dependencyVersion = getAllDependencySignals(packageJson)[dependencyName];
return typeof dependencyVersion === 'string' ? dependencyVersion : null;
}
function getVersionSignalFromManifests(dir, topDir, dependencyName, fallbackSignal) {
// Walk up nearest-first. At each node manifest, prefer the primary signal
// (e.g. @types/node — catalog/workspace-resolved by the npm resolver) over the
// fallback (engines.node, read from the same manifest's PackageJson). The
// first valid primary signal wins; otherwise the first valid fallback wins; if
// neither is valid, continue walking parents. This lets a parent package's
// engines.node be picked up when the nested package declares neither signal,
// or only unusable fallbacks like "*" / "latest".
// Deno manifests are skipped — @types/node and engines.node are npm concepts.
const lookupKey = dependencyName.startsWith(types_js_1.DEFINITELY_TYPED)
? dependencyName.substring(types_js_1.DEFINITELY_TYPED.length)
: dependencyName;
for (const manifest of (0, all_in_parent_dirs_js_1.getDependencyManifests)((0, files_js_1.normalizeToAbsolutePath)(dir), (0, files_js_1.normalizeToAbsolutePath)(topDir), node_fs_1.default)) {
if (manifest.type !== 'package-json') {
continue;
}
const version = manifest.dependencies.get(lookupKey) ?? null;
if (isValidDependencySignal(version)) {
return version;
}
if (fallbackSignal) {
const fb = fallbackSignal(manifest.manifest);
if (isValidDependencySignal(fb)) {
return fb;
}
}
}
return null;
}
function isValidDependencySignal(versionSignal) {
return versionSignal !== null && versionSignal !== 'latest' && versionSignal !== '*';
}
function hasTypeScriptNativePreviewSignal(packageJson) {
return isValidDependencySignal(getDependencyVersionSignal(packageJson, '@typescript/native-preview'));
}
function getTypeScriptVersionSignalsFromPackageJson(packageJson) {
const result = [];
const nativePreviewVersion = getDependencyVersionSignal(packageJson, '@typescript/native-preview');
if (isValidDependencySignal(nativePreviewVersion)) {
result.push(nativePreviewVersion);
}
const typeScriptVersion = getDependencyVersionSignal(packageJson, 'typescript');
if (isValidDependencySignal(typeScriptVersion)) {
result.push(typeScriptVersion);
}
return result;
}
function getTypeScriptSignalsFromPackageJsonFiles(packageJsonFiles) {
const typeScriptVersionSignals = [];
let hasTypeScriptNativePreview = false;
for (const packageJsonFile of packageJsonFiles) {
const packageJson = (0, parsed_dependency_files_js_1.parsePackageJsonContent)(packageJsonFile.fileContent);
if (packageJson === undefined) {
continue;
}
typeScriptVersionSignals.push(...getTypeScriptVersionSignalsFromPackageJson(packageJson));
hasTypeScriptNativePreview =
hasTypeScriptNativePreview || hasTypeScriptNativePreviewSignal(packageJson);
}
return { typeScriptVersionSignals, hasTypeScriptNativePreview };
}
/**
* Gets a Node.js version signal by walking up from `dir` toward `topDir`,
* picking the closest package.json with @types/node (preferred) or engines.node.
*
* @param dir directory to start the upward search from (e.g. tsconfig dir, file dir)
* @param topDir analysis base directory; walk stops here. Defaults to `dir` to
* preserve the legacy "manifest at exactly this dir" semantics for callers
* that don't yet provide a separate analysis root.
* @returns raw version string from @types/node or engines.node, or null if not found
*/
function getNodeVersionSignal(dir, topDir = dir) {
return getVersionSignalFromManifests(dir, topDir, '@types/node', packageJson => {
const enginesNode = packageJson.engines?.['node'];
return typeof enginesNode === 'string' ? enginesNode : null;
});
}
/**
* Gets a TypeScript version signal from the closest package.json walking up
* from `dir` to `topDir`. Defaults `topDir = dir` for backward compatibility.
*
* @returns raw version string from typescript dependency, or null if not found
*/
function getTypeScriptVersionSignal(dir, topDir = dir) {
return getVersionSignalFromManifests(dir, topDir, 'typescript');
}