Skip to content

Commit 285d922

Browse files
authored
JS-757 Refactor package.json cache (#5697)
1 parent d3f4632 commit 285d922

File tree

29 files changed

+1074
-406
lines changed

29 files changed

+1074
-406
lines changed

packages/jsts/src/analysis/analyzer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ import { SymbolHighlight } from '../linter/visitors/symbol-highlighting.js';
3131
import { computeMetrics, findNoSonarLines } from '../linter/visitors/metrics/index.js';
3232
import { getSyntaxHighlighting } from '../linter/visitors/syntax-highlighting.js';
3333
import { getCpdTokens } from '../linter/visitors/cpd.js';
34-
import { clearDependenciesCache } from '../rules/helpers/index.js';
3534
import { fillFileContent } from '../../../shared/src/types/analysis.js';
3635
import { shouldIgnoreFile } from '../../../shared/src/helpers/filter/filter.js';
3736
import { setGlobalConfiguration } from '../../../shared/src/helpers/configuration.js';
37+
import { clearDependenciesCache } from '../rules/helpers/package-jsons/index.js';
3838

3939
/**
4040
* Analyzes a JavaScript / TypeScript analysis input

packages/jsts/src/analysis/projectAnalysis/file-stores/package-jsons.ts

Lines changed: 22 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,39 +15,34 @@
1515
* along with this program; if not, see https://sonarsource.com/license/ssal/
1616
*/
1717

18-
import type { PackageJson } from 'type-fest';
1918
import { getFsEvents } from '../../../../../shared/src/helpers/configuration.js';
2019
import { basename, dirname } from 'node:path/posix';
21-
import {
22-
clearDependenciesCache,
23-
fillCacheWithNewPath,
24-
PACKAGE_JSON,
25-
} from '../../../rules/helpers/index.js';
20+
import { readFile } from 'node:fs/promises';
2621
import type { Dirent } from 'node:fs';
2722
import { warn, debug } from '../../../../../shared/src/helpers/logging.js';
2823
import { FileStore } from './store-type.js';
29-
import { readFile } from '../../../../../shared/src/helpers/files.js';
24+
import type { File } from '../../../rules/helpers/files.js';
25+
import {
26+
clearDependenciesCache,
27+
fillPackageJsonCaches,
28+
PACKAGE_JSON,
29+
} from '../../../rules/helpers/package-jsons/index.js';
3030

3131
export const UNINITIALIZED_ERROR =
3232
'package.json cache has not been initialized. Call loadFiles() first.';
3333

34-
type PackageJsonWithPath = {
35-
filePath: string;
36-
fileContent: PackageJson;
37-
};
38-
3934
export class PackageJsonStore implements FileStore {
40-
private packageJsons: PackageJsonWithPath[] | undefined = undefined;
35+
private readonly packageJsons: Map<string, File> = new Map();
4136
private baseDir: string | undefined = undefined;
42-
private readonly paths = new Set<string>();
37+
private readonly dirnameToParent: Map<string, string | undefined> = new Map();
4338

4439
async isInitialized(baseDir: string) {
4540
this.dirtyCachesIfNeeded(baseDir);
46-
return typeof this.packageJsons !== 'undefined';
41+
return typeof this.baseDir !== 'undefined';
4742
}
4843

4944
getPackageJsons() {
50-
if (!this.packageJsons) {
45+
if (!this.baseDir) {
5146
throw new Error(UNINITIALIZED_ERROR);
5247
}
5348
return this.packageJsons;
@@ -68,48 +63,41 @@ export class PackageJsonStore implements FileStore {
6863
}
6964

7065
clearCache() {
71-
this.packageJsons = undefined;
7266
this.baseDir = undefined;
73-
this.paths.clear();
67+
this.packageJsons.clear();
68+
this.dirnameToParent.clear();
7469
debug('Clearing dependencies cache');
7570
clearDependenciesCache();
7671
}
7772

7873
setup(baseDir: string) {
7974
this.baseDir = baseDir;
80-
this.paths.add(baseDir);
81-
this.packageJsons = [];
75+
this.dirnameToParent.set(baseDir, undefined);
8276
}
8377

8478
async processFile(file: Dirent, filePath: string) {
85-
if (!this.packageJsons) {
79+
if (!this.baseDir) {
8680
throw new Error(UNINITIALIZED_ERROR);
8781
}
8882
if (file.name === PACKAGE_JSON) {
8983
try {
90-
const fileContent = JSON.parse(await readFile(filePath));
91-
this.packageJsons.push({ filePath, fileContent });
84+
const content = await readFile(filePath, 'utf-8');
85+
this.packageJsons.set(dirname(filePath), { content, path: filePath });
9286
} catch (e) {
93-
warn(`Error parsing package.json ${filePath}: ${e}`);
87+
warn(`Error reading package.json ${filePath}: ${e}`);
9488
}
9589
}
9690
}
9791

9892
processDirectory(dir: string) {
99-
this.paths.add(dir);
93+
const parent = dirname(dir);
94+
this.dirnameToParent.set(dir, parent);
10095
}
10196

10297
async postProcess() {
103-
if (!this.packageJsons) {
98+
if (!this.baseDir) {
10499
throw new Error(UNINITIALIZED_ERROR);
105100
}
106-
for (const projectPath of this.paths) {
107-
fillCacheWithNewPath(
108-
projectPath,
109-
this.packageJsons
110-
.filter(({ filePath }) => projectPath.startsWith(dirname(filePath)))
111-
.map(({ fileContent }) => fileContent),
112-
);
113-
}
101+
fillPackageJsonCaches(this.packageJsons, this.dirnameToParent, this.baseDir);
114102
}
115103
}

packages/jsts/src/linter/linter.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { FileType } from '../../../shared/src/helpers/files.js';
2323
import { LintingResult, transformMessages } from './issues/transform.js';
2424
import { customRules } from './custom-rules/rules.js';
2525
import * as internalRules from '../rules/rules.js';
26-
import { getClosestPackageJSONDir, getDependencies, toUnixPath } from '../rules/helpers/index.js';
26+
import { toUnixPath } from '../rules/helpers/index.js';
2727
import { createOptions } from './pragmas.js';
2828
import path from 'path';
2929
import { ParseResult } from '../parsers/parse.js';
@@ -32,9 +32,11 @@ import globalsPkg from 'globals';
3232
import { APIError } from '../../../shared/src/errors/error.js';
3333
import { pathToFileURL } from 'node:url';
3434
import * as ruleMetas from '../rules/metas.js';
35-
import { extname } from 'node:path/posix';
35+
import { extname, dirname } from 'node:path/posix';
3636
import { defaultOptions } from '../rules/helpers/configs.js';
3737
import merge from 'lodash.merge';
38+
import { getDependencies } from '../rules/helpers/package-jsons/dependencies.js';
39+
import { getClosestPackageJSONDir } from '../rules/helpers/package-jsons/closest.js';
3840

3941
interface InitializeParams {
4042
rules?: RuleConfig[];
@@ -251,7 +253,7 @@ export class Linter {
251253
* The wrapper's linting configuration includes multiple ESLint
252254
* configurations: one per fileType/language/analysisMode combination.
253255
*/
254-
const dependencies = getDependencies(filePath, Linter.baseDir);
256+
const dependencies = getDependencies(dirname(filePath), Linter.baseDir);
255257
const rules = Linter.ruleConfigs?.filter(ruleConfig => {
256258
const {
257259
fileTypeTargets,
@@ -337,6 +339,6 @@ function createLinterConfigKey(
337339
language: JsTsLanguage,
338340
analysisMode: AnalysisMode,
339341
) {
340-
const packageJsonDirName = getClosestPackageJSONDir(filePath, baseDir);
342+
const packageJsonDirName = getClosestPackageJSONDir(dirname(filePath), baseDir);
341343
return `${fileType}-${language}-${analysisMode}-${extname(toUnixPath(filePath))}-${packageJsonDirName}`;
342344
}

packages/jsts/src/rules/S1607/rule.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ import type { Rule } from 'eslint';
2020
import type estree from 'estree';
2121
import {
2222
generateMeta,
23-
getDependencies,
2423
getFullyQualifiedName,
25-
getManifests,
2624
getProperty,
2725
getValueOfExpression,
2826
isFunctionInvocation,
@@ -34,7 +32,9 @@ import {
3432
} from '../helpers/index.js';
3533
import * as meta from './generated-meta.js';
3634
import type { TSESTree } from '@typescript-eslint/utils';
37-
import { dirname } from 'path/posix';
35+
import { dirname } from 'node:path';
36+
import { getDependencies } from '../helpers/package-jsons/dependencies.js';
37+
import { getManifests } from '../helpers/package-jsons/all-in-parent-dirs.js';
3838

3939
export const rule: Rule.RuleModule = {
4040
meta: generateMeta(meta, {
@@ -43,7 +43,7 @@ export const rule: Rule.RuleModule = {
4343
},
4444
}),
4545
create(context) {
46-
const dependencies = getDependencies(context.filename, context.cwd);
46+
const dependencies = getDependencies(dirname(context.filename), context.cwd);
4747
switch (true) {
4848
case dependencies.has('jasmine'):
4949
return jasmineListener();

packages/jsts/src/rules/S4328/rule.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ import type { Rule } from 'eslint';
2020
import type estree from 'estree';
2121
import builtins from 'builtin-modules';
2222
import ts from 'typescript';
23-
import { generateMeta, getDependencies } from '../helpers/index.js';
23+
import { generateMeta } from '../helpers/index.js';
2424
import { FromSchema } from 'json-schema-to-ts';
2525
import * as meta from './generated-meta.js';
2626
import { Minimatch } from 'minimatch';
27+
import { dirname } from 'node:path';
28+
import { getDependencies } from '../helpers/package-jsons/dependencies.js';
2729

2830
const messages = {
2931
removeOrAddDependency: 'Either remove this import or add it as a dependency.',
@@ -33,7 +35,7 @@ export const rule: Rule.RuleModule = {
3335
meta: generateMeta(meta, { messages }),
3436
create(context: Rule.RuleContext) {
3537
// we need to find all the npm manifests from the directory of the analyzed file to the context working directory
36-
const dependencies = getDependencies(context.filename, context.cwd);
38+
const dependencies = getDependencies(dirname(context.filename), context.cwd);
3739

3840
const whitelist = (context.options as FromSchema<typeof meta.schema>)[0]?.whitelist || [];
3941
const program = context.sourceCode.parserServices?.program;

packages/jsts/src/rules/S5973/rule.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919
import type { Rule } from 'eslint';
2020
import {
2121
generateMeta,
22-
getDependencies,
2322
getFullyQualifiedName,
2423
isIdentifier,
2524
isMethodInvocation,
2625
Mocha,
2726
} from '../helpers/index.js';
2827
import type estree from 'estree';
2928
import * as meta from './generated-meta.js';
29+
import { dirname } from 'node:path';
30+
import { getDependencies } from '../helpers/package-jsons/dependencies.js';
3031

3132
export const rule: Rule.RuleModule = {
3233
meta: generateMeta(meta, {
@@ -79,7 +80,7 @@ function hasJestRetry(context: Rule.RuleContext, node: estree.CallExpression, ha
7980
}
8081

8182
function hasJestDependency(context: Rule.RuleContext) {
82-
const dependencies = getDependencies(context.filename, context.cwd);
83+
const dependencies = getDependencies(dirname(context.filename), context.cwd);
8384
return dependencies.has('jest');
8485
}
8586

packages/jsts/src/rules/S6477/rule.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
// https://sonarsource.github.io/rspec/#/rspec/S6477/javascript
1818

1919
import type { Rule } from 'eslint';
20+
import { dirname } from 'node:path';
2021
import { rules } from '../external/react.js';
21-
import { generateMeta, getDependencies } from '../helpers/index.js';
22+
import { generateMeta } from '../helpers/index.js';
2223
import { decorate } from './decorator.js';
2324
import * as meta from './generated-meta.js';
25+
import { getDependencies } from '../helpers/package-jsons/dependencies.js';
2426

2527
const decoratedJsxKey = decorate(rules['jsx-key']);
2628

@@ -31,7 +33,7 @@ export const rule: Rule.RuleModule = {
3133
},
3234
}),
3335
create(context: Rule.RuleContext) {
34-
const dependencies = getDependencies(context.filename, context.cwd);
36+
const dependencies = getDependencies(dirname(context.filename), context.cwd);
3537
if (!dependencies.has('react')) {
3638
return {};
3739
}

packages/jsts/src/rules/S6747/rule.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
// https://sonarsource.github.io/rspec/#/rspec/S6747/javascript
1818

1919
import type { Rule } from 'eslint';
20+
import { dirname } from 'node:path';
2021
import { rules as reactRules } from '../external/react.js';
2122
import { rules as jsxA11yRules } from '../external/a11y.js';
22-
import { generateMeta, getDependencies, interceptReport, mergeRules } from '../helpers/index.js';
23+
import { generateMeta, interceptReport, mergeRules } from '../helpers/index.js';
2324
import { decorate } from './decorator.js';
2425
import type { TSESTree } from '@typescript-eslint/utils';
2526
import * as meta from './generated-meta.js';
27+
import { getDependencies } from '../helpers/package-jsons/dependencies.js';
2628

2729
const noUnknownProp = reactRules['no-unknown-property'];
2830
const decoratedNoUnknownProp = decorate(noUnknownProp);
@@ -85,7 +87,7 @@ export const rule: Rule.RuleModule = {
8587
}),
8688

8789
create(context: Rule.RuleContext) {
88-
const dependencies = getDependencies(context.filename, context.cwd);
90+
const dependencies = getDependencies(dirname(context.filename), context.cwd);
8991
if (!dependencies.has('react')) {
9092
return {};
9193
}

packages/jsts/src/rules/S6957/rule.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818

1919
import type { Rule } from 'eslint';
2020
import { rules } from '../external/react.js';
21-
import { generateMeta, getManifests, toUnixPath } from '../helpers/index.js';
21+
import { generateMeta, toUnixPath } from '../helpers/index.js';
2222
import { FromSchema } from 'json-schema-to-ts';
2323
import * as meta from './generated-meta.js';
2424
import { dirname } from 'path/posix';
25+
import { getManifests } from '../helpers/package-jsons/all-in-parent-dirs.js';
2526

2627
const reactNoDeprecated = rules['no-deprecated'];
2728

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* SonarQube JavaScript Plugin
3+
* Copyright (C) 2011-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
18+
export class ComputedCache<K, V, TContext = null> {
19+
private readonly cache: Map<K, V>;
20+
21+
constructor(private readonly computeFn: (key: K, context?: TContext) => V) {
22+
this.cache = new Map();
23+
}
24+
25+
get(key: K, context?: TContext) {
26+
if (this.cache.has(key)) {
27+
return this.cache.get(key)!;
28+
}
29+
30+
const value = this.computeFn(key, context);
31+
this.cache.set(key, value);
32+
return value;
33+
}
34+
35+
set(key: K, value: V) {
36+
this.cache.set(key, value);
37+
}
38+
39+
has(key: K) {
40+
return this.cache.has(key);
41+
}
42+
43+
delete(key: K) {
44+
return this.cache.delete(key);
45+
}
46+
47+
clear() {
48+
this.cache.clear();
49+
}
50+
51+
get size() {
52+
return this.cache.size;
53+
}
54+
}

0 commit comments

Comments
 (0)