eslint-plugin-sonarjs
Version:
295 lines (294 loc) • 13.1 kB
JavaScript
;
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;
}