UNPKG

eslint-plugin-sonarjs

Version:
256 lines (255 loc) 11.8 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.getReactVersion = getReactVersion; exports.parseReactVersion = parseReactVersion; 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 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 dirname. Exported for tests */ exports.dependenciesCache = new cache_js_1.ComputedCache((dir, topDir) => { const closestDependencyManifestDir = (0, closest_js_1.getClosestDependencyManifestDir)(dir, topDir); const result = new Map(); if (!closestDependencyManifestDir) { return result; } for (const { dependencies } of (0, all_in_parent_dirs_js_1.getDependencyManifests)(closestDependencyManifestDir, 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. if (merged.has(name)) { const manifestVersion = merged.get(name); if (manifestVersion !== inlineVersion) { console.debug(`Dependency "${typeof name === 'string' ? name : name.pattern}" has a version conflict between the manifest ` + `(${manifestVersion ?? '<unspecified>'}) and an inline npm: import ` + `(${inlineVersion ?? '<unspecified>'}). Using the inline version.`); } } merged.set(name, inlineVersion); } return merged; } function getDependenciesSanitizePaths(context) { return withCurrentFileInlineDependencies(getDependencies((0, files_js_1.dirnamePath)((0, files_js_1.normalizeToAbsolutePath)(context.filename)), (0, files_js_1.normalizeToAbsolutePath)(context.cwd))); } /** * 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 dir = (0, files_js_1.dirnamePath)((0, files_js_1.normalizeToAbsolutePath)(context.filename)); const dependencies = withCurrentFileInlineDependencies(getDependencies(dir, (0, files_js_1.normalizeToAbsolutePath)(context.cwd))); 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) { try { // Coerce version ranges (e.g., "^18.0.0") to valid semver versions return (0, semver_1.minVersion)(reactVersion)?.version ?? null; } catch { // Handle non-semver strings like pnpm catalog references (e.g., "catalog:frontend") 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'); }