Repository: SonarSource/SonarJS
Pull request #5404: JS-734 Search for package.json files during project lookup
Author: login: vdiez
Created: 2025-05-21T17:10:29Z | Merged: 2025-05-22T09:22:22Z

[JS-734](https://sonarsource.atlassian.net/browse/JS-734)



[JS-734]: https://sonarsource.atlassian.net/browse/JS-734?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/packages/jsts/src/analysis/projectAnalysis/files-finder.ts b/packages/jsts/src/analysis/projectAnalysis/files-finder.ts
index dfbcbd62e07..ce34814d7e9 100644
--- a/packages/jsts/src/analysis/projectAnalysis/files-finder.ts
+++ b/packages/jsts/src/analysis/projectAnalysis/files-finder.ts
@@ -31,13 +31,19 @@ import { readFile } from 'node:fs/promises';
 import { accept } from './filter/filter.js';
 import { initializeTsConfigs, TSCONFIG_JSON, tsConfigCacheInitialized } from './tsconfigs.js';
 import { fileCacheInitialized, setFiles } from './files.js';
-import { error, info } from '../../../../shared/src/helpers/logging.js';
+import { error, info, warn } from '../../../../shared/src/helpers/logging.js';
 import { Minimatch } from 'minimatch';
+import {
+  packageJsonsCacheInitialized,
+  PackageJsonWithPath,
+  setPackageJsons,
+} from './package-jsons.js';
+import { PACKAGE_JSON, stripBOM } from '../../rules/index.js';
 
 type FilterSearch = {
   files: boolean;
   tsconfigs: boolean;
-  //we can add package.json search to the same search
+  packageJsons: boolean;
 };
 
 type ProvidedTsConfig = {
@@ -47,8 +53,9 @@ type ProvidedTsConfig = {
 
 export async function loadFiles(baseDir: string, inputFiles?: JsTsFiles) {
   const filterSearch: FilterSearch = {
-    files: shouldSearchInputFiles(inputFiles),
+    files: shouldSearchInputFiles(baseDir, inputFiles),
     tsconfigs: !tsConfigCacheInitialized(baseDir),
+    packageJsons: !packageJsonsCacheInitialized(baseDir),
   };
   // if all filters are off, skip search
   if (Object.values(filterSearch).every(value => !value)) {
@@ -69,6 +76,7 @@ export async function loadFiles(baseDir: string, inputFiles?: JsTsFiles) {
   const testPaths = getTestPaths()?.map(test => toUnixPath(join(baseDir, test)));
 
   const files: JsTsFiles = {};
+  const foundPackageJsons: PackageJsonWithPath[] = [];
   // tsconfig.json files in the project tree
   const foundLookupTsConfigs: string[] = [];
   // tsconfig.json files specified in sonar.typescript.tsconfigPaths
@@ -101,11 +109,23 @@ export async function loadFiles(baseDir: string, inputFiles?: JsTsFiles) {
           foundLookupTsConfigs.push(filePath);
         }
       }
+
+      if (filterSearch.packageJsons && file.name === PACKAGE_JSON) {
+        try {
+          const fileContent = JSON.parse(stripBOM(await readFile(filePath, 'utf8')));
+          foundPackageJsons.push({ filePath, fileContent });
+        } catch (e) {
+          warn(`Error parsing package.json ${filePath}: ${e}`);
+        }
+      }
     },
     getExclusions(),
   );
   if (filterSearch.files) {
-    setFiles(files);
+    setFiles(baseDir, files);
+  }
+  if (filterSearch.packageJsons) {
+    setPackageJsons(baseDir, foundPackageJsons);
   }
   if (filterSearch.tsconfigs) {
     if (getTsConfigPaths().length && !foundPropertyTsConfigs.length) {
@@ -127,11 +147,11 @@ function getFiletype(filePath: string, testPaths?: string[]): FileType {
   return 'MAIN';
 }
 
-function shouldSearchInputFiles(files?: JsTsFiles) {
+function shouldSearchInputFiles(baseDir: string, files?: JsTsFiles) {
   if (!isSonarLint() && files) {
-    setFiles(files);
+    setFiles(baseDir, files);
     return false;
   }
-  // we just need the file cache to know how many are there to enable or disable type-checking
-  return !fileCacheInitialized();
+  // in sonarlint we just need the file cache to know how many are there to enable or disable type-checking
+  return !fileCacheInitialized(baseDir);
 }
diff --git a/packages/jsts/src/analysis/projectAnalysis/files.ts b/packages/jsts/src/analysis/projectAnalysis/files.ts
index ce39417c88a..7a53cd8eb23 100644
--- a/packages/jsts/src/analysis/projectAnalysis/files.ts
+++ b/packages/jsts/src/analysis/projectAnalysis/files.ts
@@ -16,18 +16,24 @@
  */
 
 import type { JsTsFiles } from './projectAnalysis.js';
+import { toUnixPath } from '../../rules/index.js';
+import { dirname } from 'node:path/posix';
 
 export const UNINITIALIZED_ERROR = 'Files cache has not been initialized. Call loadFiles() first.';
 
+let baseDir: string | undefined;
 let files: JsTsFiles | undefined;
 let filenames: string[] | undefined;
+let paths: Set<string> | undefined;
 
 export function clearFilesCache() {
   files = undefined;
   filenames = undefined;
+  paths = undefined;
 }
 
-export function fileCacheInitialized() {
+export function fileCacheInitialized(baseDir: string) {
+  dirtyCachesIfNeeded(baseDir);
   return typeof files !== 'undefined';
 }
 
@@ -52,7 +58,29 @@ export function getFilenames() {
   return filenames;
 }
 
-export function setFiles(newFiles: JsTsFiles) {
-  files = newFiles;
-  filenames = Object.keys(newFiles);
+export function getPaths() {
+  if (!paths) {
+    throw new Error(UNINITIALIZED_ERROR);
+  }
+  return paths;
+}
+
+export function setFiles(newBaseDir: string, newFiles: JsTsFiles) {
+  baseDir = newBaseDir;
+  files = {};
+  filenames = [];
+  paths = new Set<string>();
+  Object.entries(newFiles).forEach(([rawFilename, file]) => {
+    const filename = toUnixPath(rawFilename);
+    paths!.add(dirname(filename));
+    filenames!.push(filename);
+    file.filePath = filename;
+    files![filename] = file;
+  });
+}
+
+function dirtyCachesIfNeeded(currentBaseDir: string) {
+  if (currentBaseDir !== baseDir) {
+    clearFilesCache();
+  }
 }
diff --git a/packages/jsts/src/analysis/projectAnalysis/package-jsons.ts b/packages/jsts/src/analysis/projectAnalysis/package-jsons.ts
new file mode 100644
index 00000000000..dba1b08611d
--- /dev/null
+++ b/packages/jsts/src/analysis/projectAnalysis/package-jsons.ts
@@ -0,0 +1,77 @@
+/*
+ * SonarQube JavaScript Plugin
+ * Copyright (C) 2011-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
+ *
+ * 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/
+ */
+
+import { PackageJson } from 'type-fest';
+import { getFsEvents } from '../../../../shared/src/helpers/configuration.js';
+import { basename, dirname } from 'node:path/posix';
+import { clearDependenciesCache, fillCacheWithNewPath, PACKAGE_JSON } from '../../rules/index.js';
+import { getPaths } from './files.js';
+
+export const UNINITIALIZED_ERROR =
+  'package.json cache has not been initialized. Call loadFiles() first.';
+
+export type PackageJsonWithPath = {
+  filePath: string;
+  fileContent: PackageJson;
+};
+
+let packageJsons: PackageJsonWithPath[] | undefined;
+let baseDir: string | undefined;
+
+export function packageJsonsCacheInitialized(baseDir: string) {
+  dirtyCachesIfNeeded(baseDir);
+  return typeof packageJsons !== 'undefined';
+}
+
+export function getPackageJsons() {
+  if (!packageJsons) {
+    throw new Error(UNINITIALIZED_ERROR);
+  }
+  return packageJsons;
+}
+
+export function setPackageJsons(newBaseDir: string, newPackageJsons: PackageJsonWithPath[]) {
+  packageJsons = newPackageJsons;
+  baseDir = newBaseDir;
+  for (const projectPath of getPaths()) {
+    fillCacheWithNewPath(
+      projectPath,
+      packageJsons
+        .filter(({ filePath }) => projectPath.startsWith(dirname(filePath)))
+        .map(({ fileContent }) => fileContent),
+    );
+  }
+}
+
+export function dirtyCachesIfNeeded(currentBaseDir: string) {
+  if (currentBaseDir !== baseDir) {
+    clearPackageJsonsCache();
+  }
+  for (const fileEvent of getFsEvents()) {
+    const [filename] = fileEvent;
+    const filenameLower = basename(filename).toLowerCase();
+    if (filenameLower === PACKAGE_JSON) {
+      clearPackageJsonsCache();
+    }
+  }
+}
+
+export function clearPackageJsonsCache() {
+  packageJsons = undefined;
+  baseDir = undefined;
+  clearDependenciesCache();
+}
diff --git a/packages/jsts/src/analysis/projectAnalysis/projectAnalyzer.ts b/packages/jsts/src/analysis/projectAnalysis/projectAnalyzer.ts
index 40d5cdd4820..c9f5c5285b1 100644
--- a/packages/jsts/src/analysis/projectAnalysis/projectAnalyzer.ts
+++ b/packages/jsts/src/analysis/projectAnalysis/projectAnalyzer.ts
@@ -76,7 +76,13 @@ export async function analyzeProject(input: ProjectAnalysisInput): Promise<Proje
       info(
         `Found ${pendingFiles.size} file(s) not part of any tsconfig.json: they will be analyzed without type information`,
       );
-      await analyzeWithoutProgram(pendingFiles, filesToAnalyze, results, baseDir, progressReport);
+      await analyzeWithoutProgram(
+        pendingFiles,
+        filesToAnalyze,
+        results,
+        normalizedBaseDir,
+        progressReport,
+      );
     }
   }
   progressReport.stop();
diff --git a/packages/jsts/src/analysis/projectAnalysis/tsconfigs.ts b/packages/jsts/src/analysis/projectAnalysis/tsconfigs.ts
index 78a32276de9..23b4e335c91 100644
--- a/packages/jsts/src/analysis/projectAnalysis/tsconfigs.ts
+++ b/packages/jsts/src/analysis/projectAnalysis/tsconfigs.ts
@@ -29,14 +29,12 @@ import {
   isJsTsFile,
   isSonarLint,
   maxFilesForTypeChecking,
-  setClearDependenciesCache,
   setClearFileToTsConfigCache,
   setClearTsConfigCache,
   shouldClearFileToTsConfigCache,
   shouldClearTsConfigCache,
 } from '../../../../shared/src/helpers/configuration.js';
 import { basename } from 'node:path';
-import { PACKAGE_JSON } from '../../rules/index.js';
 
 tmp.setGracefulCleanup();
 export const UNINITIALIZED_ERROR =
@@ -155,8 +153,6 @@ export function dirtyCachesIfNeeded(baseDir: string) {
     if (filenameLower.endsWith('.json') && filenameLower.includes('tsconfig')) {
       changedTsConfigs.push(filename);
       setClearTsConfigCache(true);
-    } else if (filenameLower === PACKAGE_JSON) {
-      setClearDependenciesCache(true);
     } else if (isJsTsFile(filename) && event === 'CREATED') {
       setClearFileToTsConfigCache(true);
     }
diff --git a/packages/jsts/src/rules/helpers/package-json.ts b/packages/jsts/src/rules/helpers/package-json.ts
index cdfed74d4eb..0872094bddd 100644
--- a/packages/jsts/src/rules/helpers/package-json.ts
+++ b/packages/jsts/src/rules/helpers/package-json.ts
@@ -43,9 +43,9 @@ export type NamedDependency = {
 type Dependency = MinimatchDependency | NamedDependency;
 
 /**
- * Cache for the available dependencies by dirname.
+ * Cache for the available dependencies by dirname. Exported for tests
  */
-const cache: Map<string, Set<Dependency>> = new Map();
+export const cache: Map<string, Set<Dependency>> = new Map();
 
 /**
  * Returns the dependencies of the root package.json file collected in the cache.
@@ -76,14 +76,17 @@ export function getAllDependencies(): NamedDependency[] {
  */
 export function getDependencies(filename: string, cwd: string) {
   const dirname = Path.dirname(toUnixPath(filename));
-  const cached = cache.get(dirname);
-  if (cached) {
-    return new Set([...cached].map(item => item.name));
+  if (!cache.get(dirname)) {
+    fillCacheWithNewPath(dirname, getManifests(dirname, cwd, fs));
   }
+  return new Set([...cache.get(dirname)!].map(item => item.name));
+}
+
+export function fillCacheWithNewPath(dirname: string, manifests: PackageJson[]) {
   const result = new Set<Dependency>();
   cache.set(dirname, result);
 
-  getManifests(dirname, cwd, fs).forEach(manifest => {
+  manifests.forEach(manifest => {
     const manifestDependencies = getDependenciesFromPackageJson(manifest);
 
     manifestDependencies.forEach(dependency => {
diff --git a/packages/jsts/tests/analysis/fixtures/package-json-malformed/package.json b/packages/jsts/tests/analysis/fixtures/package-json-malformed/package.json
new file mode 100644
index 00000000000..456fff61db1
--- /dev/null
+++ b/packages/jsts/tests/analysis/fixtures/package-json-malformed/package.json
@@ -0,0 +1,5 @@
+{
+  "name": "test-module",
+  "version": "1.0.0",
+  "author": "Your Name <email@example.com>"
+
diff --git a/packages/jsts/tests/analysis/package-jsons.test.ts b/packages/jsts/tests/analysis/package-jsons.test.ts
new file mode 100644
index 00000000000..fb7804dea2d
--- /dev/null
+++ b/packages/jsts/tests/analysis/package-jsons.test.ts
@@ -0,0 +1,94 @@
+/*
+ * SonarQube JavaScript Plugin
+ * Copyright (C) 2011-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
+ *
+ * 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/
+ */
+import { beforeEach, describe, it, type Mock } from 'node:test';
+import { expect } from 'expect';
+import { join } from 'node:path/posix';
+import { loadFiles } from '../../src/analysis/projectAnalysis/files-finder.js';
+import {
+  clearPackageJsonsCache,
+  getPackageJsons,
+  packageJsonsCacheInitialized,
+  dirtyCachesIfNeeded,
+  UNINITIALIZED_ERROR,
+} from '../../src/analysis/projectAnalysis/package-jsons.js';
+import { readFile } from 'node:fs/promises';
+import { PackageJson } from 'type-fest';
+import { toUnixPath } from '../../../shared/src/helpers/files.js';
+import { cache } from '../../src/rules/index.js';
+import { setGlobalConfiguration } from '../../../shared/src/helpers/configuration.js';
+
+const fixtures = toUnixPath(join(import.meta.dirname, 'fixtures'));
+
+describe('files', () => {
+  beforeEach(() => {
+    clearPackageJsonsCache();
+  });
+  it('should crash getting files before initializing', async () => {
+    expect(getPackageJsons).toThrow(new Error(UNINITIALIZED_ERROR));
+  });
+
+  it('should return the package.json files', async () => {
+    await loadFiles(join(fixtures, 'dependencies'));
+    const filePath = join(fixtures, 'dependencies', 'package.json');
+    const fileContent = JSON.parse(await readFile(filePath, 'utf-8')) as PackageJson;
+    expect(getPackageJsons()).toEqual([
+      {
+        filePath,
+        fileContent,
+      },
+    ]);
+  });
+
+  it('should fill the package.json cache used for rules', async () => {
+    const path = join(fixtures, 'dependencies');
+    await loadFiles(path);
+    expect(cache.size).toEqual(1);
+    expect(cache.has(path)).toEqual(true);
+  });
+
+  it('should ignore malformed the package.json files', async ({ mock }) => {
+    mock.method(console, 'log');
+    const consoleLogMock = (console.log as Mock<typeof console.log>).mock;
+    await loadFiles(join(fixtures, 'package-json-malformed'));
+    const filePath = join(fixtures, 'package-json-malformed', 'package.json');
+    expect(getPackageJsons()).toHaveLength(0);
+
+    expect(
+      consoleLogMock.calls
+        .map(call => call.arguments[0])
+        .some(log => log.match(`Error parsing package.json ${filePath}: SyntaxError`)),
+    ).toEqual(true);
+  });
+
+  it('should clear the package.json cache', async () => {
+    setGlobalConfiguration();
+    const baseDir = join(fixtures, 'dependencies');
+    await loadFiles(baseDir);
+    expect(packageJsonsCacheInitialized(baseDir)).toEqual(true);
+    expect(getPackageJsons()).toHaveLength(1);
+    expect(cache.size).toEqual(1);
+
+    dirtyCachesIfNeeded(baseDir);
+    expect(getPackageJsons()).toHaveLength(1);
+
+    // we create a file event
+    setGlobalConfiguration({ fsEvents: [[join(baseDir, 'package.json'), 'MODIFIED']] });
+    dirtyCachesIfNeeded(baseDir);
+    expect(getPackageJsons).toThrow(new Error(UNINITIALIZED_ERROR));
+    expect(cache.size).toEqual(0);
+  });
+});


Reviews:
user: login: zglicz
body: Overall ok. As discussed offline, it'd be nice to know what type of paths we have.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T08:23:50Z
user: login: vdiez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T08:41:09Z
user: login: vdiez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T08:41:37Z
user: login: vdiez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T08:41:56Z
user: login: vdiez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T08:42:50Z
user: login: vdiez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T08:43:01Z
user: login: vdiez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T08:43:07Z
user: login: vdiez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T08:43:13Z
user: login: vdiez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T08:43:23Z

Review comments:
diff_hunk: @@ -33,10 +33,17 @@ import { initializeTsConfigs, TSCONFIG_JSON, tsConfigCacheInitialized } from './
 import { fileCacheInitialized, setFiles } from './files.js';
 import { error, info } from '../../../../shared/src/helpers/logging.js';
 import { Minimatch } from 'minimatch';
+import {
+  packageJsonsCacheInitialized,
+  PackageJsonWithPath,
+  setPackageJsons,
+} from './package-jsons.js';
+import { PACKAGE_JSON, stripBOM } from '../../rules/index.js';
 
 type FilterSearch = {
   files: boolean;
   tsconfigs: boolean;
+  packageJsons: boolean;
path: packages/jsts/src/analysis/projectAnalysis/files-finder.ts
user: login: zglicz
body: do you wish to remove the comment below?
created_at: 2025-05-22T08:02:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,11 +110,23 @@ export async function loadFiles(baseDir: string, inputFiles?: JsTsFiles) {
           foundLookupTsConfigs.push(filePath);
         }
       }
+
+      if (filterSearch.packageJsons && file.name === PACKAGE_JSON) {
+        try {
+          const fileContent = JSON.parse(stripBOM(await readFile(filePath, 'utf8')));
+          foundPackageJsons.push({ filePath, fileContent });
+        } catch (e) {
+          error(`Error parsing package.json ${filePath}: ${e}`);
path: packages/jsts/src/analysis/projectAnalysis/files-finder.ts
user: login: zglicz
body: should this be a warn? error seems to imply we can't perform the analysis
created_at: 2025-05-22T08:07:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -127,11 +148,11 @@ function getFiletype(filePath: string, testPaths?: string[]): FileType {
   return 'MAIN';
 }
 
-function shouldSearchInputFiles(files?: JsTsFiles) {
+function shouldSearchInputFiles(baseDir: string, files?: JsTsFiles) {
   if (!isSonarLint() && files) {
-    setFiles(files);
+    setFiles(baseDir, files);
     return false;
   }
-  // we just need the file cache to know how many are there to enable or disable type-checking
-  return !fileCacheInitialized();
+  // in sonarlint we just need the file cache to know how many are there to enable or disable type-checking
+  return !fileCacheInitialized(baseDir);
path: packages/jsts/src/analysis/projectAnalysis/files-finder.ts
user: login: zglicz
body: I really don't like this convoluted logic. Could you add a separate if for sonarlint() and the necessary comments
created_at: 2025-05-22T08:08:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,7 +58,29 @@ export function getFilenames() {
   return filenames;
 }
 
-export function setFiles(newFiles: JsTsFiles) {
-  files = newFiles;
-  filenames = Object.keys(newFiles);
+export function getPaths() {
+  if (!paths) {
+    throw new Error(UNINITIALIZED_ERROR);
+  }
+  return paths;
+}
+
+export function setFiles(newBaseDir: string, newFiles: JsTsFiles) {
+  baseDir = newBaseDir;
+  files = {};
+  filenames = [];
+  paths = new Set<string>();
+  Object.entries(newFiles).forEach(([rawFilename, file]) => {
+    const filename = toUnixPath(rawFilename);
+    paths!.add(dirname(filename));
+    filenames!.push(filename);
path: packages/jsts/src/analysis/projectAnalysis/files.ts
user: login: zglicz
body: why the exclamation? you define them few lines above
created_at: 2025-05-22T08:10:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,7 +58,29 @@ export function getFilenames() {
   return filenames;
 }
 
-export function setFiles(newFiles: JsTsFiles) {
-  files = newFiles;
-  filenames = Object.keys(newFiles);
+export function getPaths() {
+  if (!paths) {
+    throw new Error(UNINITIALIZED_ERROR);
+  }
+  return paths;
+}
+
+export function setFiles(newBaseDir: string, newFiles: JsTsFiles) {
+  baseDir = newBaseDir;
+  files = {};
+  filenames = [];
+  paths = new Set<string>();
+  Object.entries(newFiles).forEach(([rawFilename, file]) => {
+    const filename = toUnixPath(rawFilename);
+    paths!.add(dirname(filename));
+    filenames!.push(filename);
+    file.filePath = filename;
+    files![filename] = file;
path: packages/jsts/src/analysis/projectAnalysis/files.ts
user: login: zglicz
body: ditto
created_at: 2025-05-22T08:10:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,77 @@
+/*
+ * SonarQube JavaScript Plugin
+ * Copyright (C) 2011-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
+ *
+ * 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/
+ */
+
+import { PackageJson } from 'type-fest';
+import { getFsEvents } from '../../../../shared/src/helpers/configuration.js';
+import { basename, dirname } from 'node:path/posix';
+import { clearDependenciesCache, fillCacheWithNewPath, PACKAGE_JSON } from '../../rules/index.js';
+import { getPaths } from './files.js';
+
+export const UNINITIALIZED_ERROR =
+  'package.json cache has not been initialized. Call loadFiles() first.';
+
+export type PackageJsonWithPath = {
+  filePath: string;
path: packages/jsts/src/analysis/projectAnalysis/package-jsons.ts
user: login: zglicz
body: is this filePath normalized or not?
created_at: 2025-05-22T08:19:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,77 @@
+/*
+ * SonarQube JavaScript Plugin
+ * Copyright (C) 2011-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
+ *
+ * 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/
+ */
+
+import { PackageJson } from 'type-fest';
+import { getFsEvents } from '../../../../shared/src/helpers/configuration.js';
+import { basename, dirname } from 'node:path/posix';
+import { clearDependenciesCache, fillCacheWithNewPath, PACKAGE_JSON } from '../../rules/index.js';
+import { getPaths } from './files.js';
+
+export const UNINITIALIZED_ERROR =
+  'package.json cache has not been initialized. Call loadFiles() first.';
+
+export type PackageJsonWithPath = {
+  filePath: string;
+  fileContent: PackageJson;
+};
+
+let packageJsons: PackageJsonWithPath[] | undefined;
+let baseDir: string | undefined;
path: packages/jsts/src/analysis/projectAnalysis/package-jsons.ts
user: login: zglicz
body: is this baseDir normalized or not?
created_at: 2025-05-22T08:19:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,18 +16,24 @@
  */
 
 import type { JsTsFiles } from './projectAnalysis.js';
+import { toUnixPath } from '../../rules/index.js';
+import { dirname } from 'node:path/posix';
 
 export const UNINITIALIZED_ERROR = 'Files cache has not been initialized. Call loadFiles() first.';
 
+let baseDir: string | undefined;
 let files: JsTsFiles | undefined;
 let filenames: string[] | undefined;
+let paths: Set<string> | undefined;
path: packages/jsts/src/analysis/projectAnalysis/files.ts
user: login: zglicz
body: are these normalized?
created_at: 2025-05-22T08:20:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,18 +16,24 @@
  */
 
 import type { JsTsFiles } from './projectAnalysis.js';
+import { toUnixPath } from '../../rules/index.js';
+import { dirname } from 'node:path/posix';
 
 export const UNINITIALIZED_ERROR = 'Files cache has not been initialized. Call loadFiles() first.';
 
+let baseDir: string | undefined;
 let files: JsTsFiles | undefined;
 let filenames: string[] | undefined;
path: packages/jsts/src/analysis/projectAnalysis/files.ts
user: login: zglicz
body: are the filenames normalized?
created_at: 2025-05-22T08:20:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,9 +43,9 @@ export type NamedDependency = {
 type Dependency = MinimatchDependency | NamedDependency;
 
 /**
- * Cache for the available dependencies by dirname.
+ * Cache for the available dependencies by dirname. Exported for tests
  */
-const cache: Map<string, Set<Dependency>> = new Map();
+export const cache: Map<string, Set<Dependency>> = new Map();
path: packages/jsts/src/rules/helpers/package-json.ts
user: login: zglicz
body: are the keys here normalized?
created_at: 2025-05-22T08:21:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -127,11 +148,11 @@ function getFiletype(filePath: string, testPaths?: string[]): FileType {
   return 'MAIN';
 }
 
-function shouldSearchInputFiles(files?: JsTsFiles) {
+function shouldSearchInputFiles(baseDir: string, files?: JsTsFiles) {
   if (!isSonarLint() && files) {
-    setFiles(files);
+    setFiles(baseDir, files);
     return false;
   }
-  // we just need the file cache to know how many are there to enable or disable type-checking
-  return !fileCacheInitialized();
+  // in sonarlint we just need the file cache to know how many are there to enable or disable type-checking
+  return !fileCacheInitialized(baseDir);
path: packages/jsts/src/analysis/projectAnalysis/files-finder.ts
user: login: vdiez
body: I'll handle this in the refactor of find-files
created_at: 2025-05-22T08:41:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,7 +58,29 @@ export function getFilenames() {
   return filenames;
 }
 
-export function setFiles(newFiles: JsTsFiles) {
-  files = newFiles;
-  filenames = Object.keys(newFiles);
+export function getPaths() {
+  if (!paths) {
+    throw new Error(UNINITIALIZED_ERROR);
+  }
+  return paths;
+}
+
+export function setFiles(newBaseDir: string, newFiles: JsTsFiles) {
+  baseDir = newBaseDir;
+  files = {};
+  filenames = [];
+  paths = new Set<string>();
+  Object.entries(newFiles).forEach(([rawFilename, file]) => {
+    const filename = toUnixPath(rawFilename);
+    paths!.add(dirname(filename));
+    filenames!.push(filename);
path: packages/jsts/src/analysis/projectAnalysis/files.ts
user: login: vdiez
body: TS is not able to infer it cannot be undefined here
created_at: 2025-05-22T08:41:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,7 +58,29 @@ export function getFilenames() {
   return filenames;
 }
 
-export function setFiles(newFiles: JsTsFiles) {
-  files = newFiles;
-  filenames = Object.keys(newFiles);
+export function getPaths() {
+  if (!paths) {
+    throw new Error(UNINITIALIZED_ERROR);
+  }
+  return paths;
+}
+
+export function setFiles(newBaseDir: string, newFiles: JsTsFiles) {
+  baseDir = newBaseDir;
+  files = {};
+  filenames = [];
+  paths = new Set<string>();
+  Object.entries(newFiles).forEach(([rawFilename, file]) => {
+    const filename = toUnixPath(rawFilename);
+    paths!.add(dirname(filename));
+    filenames!.push(filename);
+    file.filePath = filename;
+    files![filename] = file;
path: packages/jsts/src/analysis/projectAnalysis/files.ts
user: login: vdiez
body: same
created_at: 2025-05-22T08:41:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,77 @@
+/*
+ * SonarQube JavaScript Plugin
+ * Copyright (C) 2011-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
+ *
+ * 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/
+ */
+
+import { PackageJson } from 'type-fest';
+import { getFsEvents } from '../../../../shared/src/helpers/configuration.js';
+import { basename, dirname } from 'node:path/posix';
+import { clearDependenciesCache, fillCacheWithNewPath, PACKAGE_JSON } from '../../rules/index.js';
+import { getPaths } from './files.js';
+
+export const UNINITIALIZED_ERROR =
+  'package.json cache has not been initialized. Call loadFiles() first.';
+
+export type PackageJsonWithPath = {
+  filePath: string;
path: packages/jsts/src/analysis/projectAnalysis/package-jsons.ts
user: login: vdiez
body: they should always be normalized. I'm trying to normalize on each entry point.
created_at: 2025-05-22T08:42:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,77 @@
+/*
+ * SonarQube JavaScript Plugin
+ * Copyright (C) 2011-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
+ *
+ * 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/
+ */
+
+import { PackageJson } from 'type-fest';
+import { getFsEvents } from '../../../../shared/src/helpers/configuration.js';
+import { basename, dirname } from 'node:path/posix';
+import { clearDependenciesCache, fillCacheWithNewPath, PACKAGE_JSON } from '../../rules/index.js';
+import { getPaths } from './files.js';
+
+export const UNINITIALIZED_ERROR =
+  'package.json cache has not been initialized. Call loadFiles() first.';
+
+export type PackageJsonWithPath = {
+  filePath: string;
+  fileContent: PackageJson;
+};
+
+let packageJsons: PackageJsonWithPath[] | undefined;
+let baseDir: string | undefined;
path: packages/jsts/src/analysis/projectAnalysis/package-jsons.ts
user: login: vdiez
body: same as before
created_at: 2025-05-22T08:43:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,18 +16,24 @@
  */
 
 import type { JsTsFiles } from './projectAnalysis.js';
+import { toUnixPath } from '../../rules/index.js';
+import { dirname } from 'node:path/posix';
 
 export const UNINITIALIZED_ERROR = 'Files cache has not been initialized. Call loadFiles() first.';
 
+let baseDir: string | undefined;
 let files: JsTsFiles | undefined;
 let filenames: string[] | undefined;
+let paths: Set<string> | undefined;
path: packages/jsts/src/analysis/projectAnalysis/files.ts
user: login: vdiez
body: same as before
created_at: 2025-05-22T08:43:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,18 +16,24 @@
  */
 
 import type { JsTsFiles } from './projectAnalysis.js';
+import { toUnixPath } from '../../rules/index.js';
+import { dirname } from 'node:path/posix';
 
 export const UNINITIALIZED_ERROR = 'Files cache has not been initialized. Call loadFiles() first.';
 
+let baseDir: string | undefined;
 let files: JsTsFiles | undefined;
 let filenames: string[] | undefined;
path: packages/jsts/src/analysis/projectAnalysis/files.ts
user: login: vdiez
body: same as before
created_at: 2025-05-22T08:43:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,9 +43,9 @@ export type NamedDependency = {
 type Dependency = MinimatchDependency | NamedDependency;
 
 /**
- * Cache for the available dependencies by dirname.
+ * Cache for the available dependencies by dirname. Exported for tests
  */
-const cache: Map<string, Set<Dependency>> = new Map();
+export const cache: Map<string, Set<Dependency>> = new Map();
path: packages/jsts/src/rules/helpers/package-json.ts
user: login: vdiez
body: same as before
created_at: 2025-05-22T08:43:23Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqube-next[bot]
created_at: 2025-05-22T09:18:28Z
body: ## [![Quality Gate passed](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://next.sonarqube.com/sonarqube/dashboard?id=org.sonarsource.javascript%3Ajavascript&pullRequest=5404) **Quality Gate passed**  
Issues  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.javascript%3Ajavascript&pullRequest=5404&resolved=false) [0 New issues](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.javascript%3Ajavascript&pullRequest=5404&resolved=false)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/fixed-16px.png '')](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.javascript%3Ajavascript&fixedInPullRequest=5404) [0 Fixed issues](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.javascript%3Ajavascript&fixedInPullRequest=5404)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/accepted-16px.png '')](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.javascript%3Ajavascript&pullRequest=5404&issueStatuses=ACCEPTED) [5 Accepted issues](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.javascript%3Ajavascript&pullRequest=5404&issueStatuses=ACCEPTED)

Measures  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/security_hotspots?id=org.sonarsource.javascript%3Ajavascript&pullRequest=5404) [0 Security Hotspots](https://next.sonarqube.com/sonarqube/security_hotspots?id=org.sonarsource.javascript%3Ajavascript&pullRequest=5404)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/dependency-risks?id=org.sonarsource.javascript%3Ajavascript&pullRequest=5404) [0 Dependency risks](https://next.sonarqube.com/sonarqube/dependency-risks?id=org.sonarsource.javascript%3Ajavascript&pullRequest=5404)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/component_measures?id=org.sonarsource.javascript%3Ajavascript&pullRequest=5404&metric=new_coverage&view=list) [97.0% Coverage on New Code](https://next.sonarqube.com/sonarqube/component_measures?id=org.sonarsource.javascript%3Ajavascript&pullRequest=5404&metric=new_coverage&view=list)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/component_measures?id=org.sonarsource.javascript%3Ajavascript&pullRequest=5404&metric=new_duplicated_lines_density&view=list) [0.0% Duplication on New Code](https://next.sonarqube.com/sonarqube/component_measures?id=org.sonarsource.javascript%3Ajavascript&pullRequest=5404&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube](https://next.sonarqube.com/sonarqube/dashboard?id=org.sonarsource.javascript%3Ajavascript&pullRequest=5404)


author_association: NONE