UNPKG

eslint-plugin-sonarjs

Version:
325 lines (324 loc) • 14.7 kB
"use strict"; 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'); }