UNPKG

eslint-plugin-sonarjs

Version:
295 lines (294 loc) • 13.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.packageJsonManifestResolver = void 0; /* * SonarQube JavaScript Plugin * Copyright (C) SonarSource Sàrl * mailto:info AT sonarsource DOT com * * You can redistribute and/or modify this program under the terms of * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the Sonar Source-Available License for more details. * * You should have received a copy of the Sonar Source-Available License * along with this program; if not, see https://sonarsource.com/license/ssal/ */ const minimatch_1 = require("minimatch"); const node_path_1 = __importDefault(require("node:path")); const files_js_1 = require("../../files.js"); const index_js_1 = require("../index.js"); const closest_js_1 = require("../../find-up/closest.js"); const helpers_js_1 = require("./helpers.js"); const parse_js_1 = require("../parse.js"); const parsed_dependency_files_js_1 = require("../parsed-dependency-files.js"); exports.packageJsonManifestResolver = { resolve(dir, topDir, fileSystem) { const packageJson = (0, helpers_js_1.getManifestFileInDir)(index_js_1.PACKAGE_JSON, dir, topDir, fileSystem); if (!packageJson) { return []; } let parsedPackageJson = (0, parsed_dependency_files_js_1.parsePackageJson)(packageJson) ?? {}; // Captured before the pnpm injection below, which would otherwise fake a workspace root. const declaresWorkspaces = !!parsedPackageJson.workspaces; // Catalog definitions follow workspace semantics and may live above the directory from which // ESLint was started. Dependency manifest collection remains bounded by topDir. const catalogSearchTopDir = (0, files_js_1.getPathRoot)(dir); const pnpmWorkspaceCache = closest_js_1.closestPatternCache.get(index_js_1.PNPM_WORKSPACE_YAML, fileSystem); const parentOfTopDir = (0, helpers_js_1.getParentDirPath)(topDir); const pnpmWorkspaceFile = pnpmWorkspaceCache.get(topDir).get(dir) ?? (topDir === catalogSearchTopDir || parentOfTopDir === null || !(0, index_js_1.canSearchFileSystemAboveManifestCache)(index_js_1.PNPM_WORKSPACE_YAML, topDir) ? undefined : pnpmWorkspaceCache.get(catalogSearchTopDir).get(parentOfTopDir)); const parsedPnpmWorkspace = pnpmWorkspaceFile ? (0, parsed_dependency_files_js_1.parsePnpmWorkspace)(pnpmWorkspaceFile) : undefined; if (parsedPnpmWorkspace) { parsedPackageJson = injectWorkspacePackages(parsedPackageJson, parsedPnpmWorkspace); } // Bun only reads catalogs from the workspace root: a package.json declaring `workspaces` is // itself a root unless an ancestor workspace already includes it, otherwise the closest // parent package.json with catalogs is the root. const isWorkspaceRoot = declaresWorkspaces && !isIncludedInAncestorWorkspace(dir, topDir, fileSystem); const closestParent = isWorkspaceRoot ? undefined : findClosestParentPackageJsonWithCatalogs(dir, topDir, fileSystem); const catalogSource = mergeCatalogSources(closestParent ? getCatalogSource(closestParent) : getCatalogSource(parsedPackageJson), parsedPnpmWorkspace); parsedPackageJson = resolveCatalogReferences(parsedPackageJson, catalogSource); // Match Node's resolution: explicit "module" is ESM, anything else (including // missing "type") is CommonJS. const moduleType = parsedPackageJson.type === 'module' ? 'module' : 'commonjs'; return [ { type: 'package-json', manifest: parsedPackageJson, dependencies: buildDependencies(parsedPackageJson), moduleType, }, ]; }, }; function buildDependencies(packageJson) { const dependencies = new Map(); const fieldsToVisit = [ 'name', 'dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies', '_moduleAliases', 'workspaces', ]; for (const field of fieldsToVisit) { if (!packageJson[field]) { continue; } if (field === 'name') { (0, parse_js_1.addDependencies)(dependencies, { [packageJson[field]]: '*' }); continue; } if (field === 'workspaces') { (0, parse_js_1.addDependenciesArray)(dependencies, Array.isArray(packageJson[field]) ? packageJson[field] : (packageJson[field]?.packages ?? [])); continue; } (0, parse_js_1.addDependencies)(dependencies, packageJson[field]); } return dependencies; } function resolveCatalogReferences(packageJson, catalogSource) { const depFields = [ 'dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies', ]; const modifiedPackageJson = { ...packageJson }; for (const field of depFields) { const deps = packageJson[field]; if (!deps) { continue; } const resolvedDeps = {}; for (const [depName, depVersion] of Object.entries(deps)) { if (typeof depVersion === 'string' && depVersion.startsWith('catalog:')) { const catalogName = depVersion.slice('catalog:'.length).trim() || 'default'; const resolvedDep = catalogName === 'default' ? catalogSource?.catalog?.[depName] : catalogSource?.catalogs?.[catalogName]?.[depName]; resolvedDeps[depName] = resolvedDep ?? depVersion; } else { resolvedDeps[depName] = depVersion ?? ''; } } modifiedPackageJson[field] = resolvedDeps; } return modifiedPackageJson; } function injectWorkspacePackages(packageJson, pnpmWorkspace) { if (!pnpmWorkspace.packages || packageJson.workspaces) { return packageJson; } return { ...packageJson, workspaces: pnpmWorkspace.packages, }; } /** * Find the closest parent package.json with catalogs defined. * @param dir Directory to start the search from * @param topDir Top directory to stop the search at * @param fileSystem Filesystem to use for the search * @returns The closest parent package.json with catalogs, or undefined if no package.json with catalogs is found */ function findClosestParentPackageJsonWithCatalogs(dir, topDir, fileSystem) { let currentDir = (0, helpers_js_1.getParentDirPath)(dir); while (currentDir !== null) { const file = closestPackageJson(currentDir, topDir, fileSystem); if (!file) { return undefined; } const ancestorDir = (0, files_js_1.dirnamePath)(file.filePath); const parsed = (0, parsed_dependency_files_js_1.parsePackageJson)(file); if (parsed && hasCatalogs(parsed) && declaresWorkspaceDir(parsed, ancestorDir, dir)) { return parsed; } currentDir = (0, helpers_js_1.getParentDirPath)(ancestorDir); } return undefined; } /** * Check whether an ancestor package.json declares this directory as one of its workspaces. * @param dir Directory of the package.json being resolved * @param topDir Top directory to stop the search at * @param fileSystem Filesystem to use for the search * @returns True when an ancestor workspace includes this directory, false otherwise */ function isIncludedInAncestorWorkspace(dir, topDir, fileSystem) { let currentDir = (0, helpers_js_1.getParentDirPath)(dir); while (currentDir !== null) { const file = closestPackageJson(currentDir, topDir, fileSystem); if (!file) { return false; } const ancestorDir = (0, files_js_1.dirnamePath)(file.filePath); const parsed = (0, parsed_dependency_files_js_1.parsePackageJson)(file); if (parsed && declaresWorkspaceDir(parsed, ancestorDir, dir)) { return true; } currentDir = (0, helpers_js_1.getParentDirPath)(ancestorDir); } return false; } /** * Use the cache populated from analysis inputs while searching inside topDir. Once the search * moves above topDir, use a filesystem-backed cache rooted at the path root. */ function closestPackageJson(from, topDir, fileSystem) { const cache = closest_js_1.closestPatternCache.get(index_js_1.PACKAGE_JSON, fileSystem); const rootDir = (0, files_js_1.getPathRoot)(from); const canSearchAboveTopDir = (0, index_js_1.canSearchFileSystemAboveManifestCache)(index_js_1.PACKAGE_JSON, topDir); if (from === topDir || from.startsWith(`${topDir}/`)) { const file = cache.get(topDir).get(from); if (file || topDir === rootDir || !canSearchAboveTopDir) { return file; } const parentOfTopDir = (0, helpers_js_1.getParentDirPath)(topDir); return parentOfTopDir === null ? undefined : cache.get(rootDir).get(parentOfTopDir); } if (!canSearchAboveTopDir) { return undefined; } return cache.get(rootDir).get(from); } function declaresWorkspaceDir(packageJson, ancestorDir, dir) { const { workspaces } = packageJson; const patterns = Array.isArray(workspaces) ? workspaces : workspaces?.packages; if (!patterns?.length || !dir.startsWith(`${ancestorDir}/`)) { return false; } const relativeDir = dir.slice(ancestorDir.length + 1); const normalizedPatterns = patterns.map(normalizeWorkspacePattern); // Bun resolves literal workspace paths before expanding workspace globs. A negated glob only // filters glob expansion, so it cannot remove a workspace explicitly listed by its path. if (normalizedPatterns.some(pattern => !hasWorkspaceGlobSyntax(pattern) && normalizeLiteralWorkspacePath(pattern) === relativeDir)) { return true; } const workspaceGlobs = normalizedPatterns.filter(hasWorkspaceGlobSyntax).map(pattern => ({ isExclusion: pattern.startsWith('!'), workspacePattern: pattern.startsWith('!') ? pattern.slice(1) : pattern, })); const matches = (workspacePattern) => new minimatch_1.Minimatch(workspacePattern, { nonegate: true }).match(relativeDir); return workspaceGlobs.some(({ isExclusion, workspacePattern }, index) => !isExclusion && matches(workspacePattern) && !workspaceGlobs .slice(index + 1) .some(({ isExclusion, workspacePattern }) => isExclusion && matches(workspacePattern))); } function normalizeWorkspacePattern(pattern) { const isExclusion = pattern.startsWith('!'); let normalizedPattern = isExclusion ? pattern.slice(1) : pattern; normalizedPattern = normalizedPattern.startsWith('./') ? normalizedPattern.slice(2) : normalizedPattern; while (normalizedPattern.endsWith('/')) { normalizedPattern = normalizedPattern.slice(0, -1); } return isExclusion ? `!${normalizedPattern}` : normalizedPattern; } function normalizeLiteralWorkspacePath(pattern) { return node_path_1.default.posix.normalize(pattern.replace(/\\(.)/g, '$1')); } function hasWorkspaceGlobSyntax(pattern) { if (pattern.startsWith('!')) { return true; } return ['*', '{', '[', '?'].some(token => containsUnescapedToken(pattern, token)); } function containsUnescapedToken(pattern, token) { for (let index = pattern.indexOf(token); index !== -1; index = pattern.indexOf(token, index + 1)) { let slashCount = 0; for (let slashIndex = index - 1; slashIndex >= 0 && pattern[slashIndex] === '\\'; slashIndex--) { slashCount++; } if (slashCount % 2 === 0) { return true; } } return false; } function hasCatalogs(packageJson) { if (packageJson.catalog || packageJson.catalogs) { return true; } const { workspaces } = packageJson; if (workspaces && !Array.isArray(workspaces)) { return !!(workspaces.catalog || workspaces.catalogs); } return false; } function getCatalogSource(packageJson) { if (!hasCatalogs(packageJson)) { return undefined; } const workspaces = Array.isArray(packageJson.workspaces) ? undefined : packageJson.workspaces; return { catalog: workspaces?.catalog ?? packageJson.catalog, catalogs: workspaces?.catalogs ?? packageJson.catalogs, }; } function mergeCatalogSources(...sources) { const catalog = sources.find(source => source?.catalog)?.catalog; const catalogs = sources.find(source => source?.catalogs)?.catalogs; return catalog || catalogs ? { catalog, catalogs } : undefined; }