Skip to content

Commit

Permalink
ESLINTJS-56 Improve the performances of package manifest search (#4840)
Browse files Browse the repository at this point in the history
Co-authored-by: Victor Diez <[email protected]>
  • Loading branch information
ericmorand-sonarsource and vdiez authored Sep 24, 2024
1 parent fbdf2c3 commit a5f02df
Show file tree
Hide file tree
Showing 10 changed files with 1,644 additions and 1,026 deletions.
2,137 changes: 1,153 additions & 984 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"jest": "29.7.0",
"jest-sonar-reporter": "2.0.0",
"json-schema-to-ts": "3.1.1",
"memfs": "^4.12.0",
"mkdirp": "3.0.1",
"node-fetch": "3.3.2",
"prettier": "3.3.3",
Expand Down
4 changes: 0 additions & 4 deletions packages/jsts/src/program/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,6 @@ export function isRootNodeModules(file: string) {
return normalizedFile.startsWith(topNodeModules);
}

export function isRoot(file: string) {
return toUnixPath(file) === toUnixPath(path.parse(file).root);
}

/**
* Any temporary file created with the `tmp` library will be removed once the Node.js process terminates.
*/
Expand Down
5 changes: 5 additions & 0 deletions packages/jsts/src/rules/helpers/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,8 @@ export function findParent(dir: string, name: string): string | null {
}
return findParent(parentDir, name);
}

export function isRoot(file: string) {
const result = path.parse(file);
return toUnixPath(file) === toUnixPath(result.root);
}
101 changes: 101 additions & 0 deletions packages/jsts/src/rules/helpers/find-up.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* SonarQube JavaScript Plugin
* Copyright (C) 2011-2024 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 GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* 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 GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as Path from 'node:path/posix';
import type { vol } from 'memfs';
import { Minimatch } from 'minimatch';
import { isRoot } from './files';

interface Stats {
isFile(): boolean;
}

export interface Filesystem {
readdirSync: (typeof vol)['readdirSync'];

readFileSync(path: string): Buffer | string;

statSync(path: string): Stats;
}

export interface File {
readonly path: string;
readonly content: Buffer | string;
}

export type FindUp = (from: string, to: string, filesystem: Filesystem) => Array<File>;

/**
* Create an instance of FindUp.
*/
export const createFindUp = (pattern: string): FindUp => {
const cache: Map<string, Array<File>> = new Map();
const matcher = new Minimatch(pattern);

const findUp: FindUp = (from, to, filesystem) => {
const results: Array<File> = [];

let cacheContent = cache.get(from);

if (cacheContent === undefined) {
cacheContent = [];

cache.set(from, cacheContent);

for (const entry of filesystem.readdirSync(from)) {
const fullEntryPath = Path.join(from, entry.toString());

const basename = Path.basename(fullEntryPath);

if (matcher.match(basename)) {
let stats: Stats;

// the resource may not be available
try {
stats = filesystem.statSync(fullEntryPath);
} catch (error) {
// todo: this is testable and should be tested
stats = {
isFile: () => false,
};
}

if (stats.isFile()) {
cacheContent.push({
path: fullEntryPath,
content: filesystem.readFileSync(fullEntryPath),
});
}
}
}
}

results.push(...cacheContent);

if (!isRoot(from) && from !== to) {
const parent = Path.dirname(from);

results.push(...findUp(parent, to, filesystem));
}

return results;
};

return findUp;
};
53 changes: 16 additions & 37 deletions packages/jsts/src/rules/helpers/package-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@ import { type PackageJson } from 'type-fest';
import { searchFiles, File } from './find-files';
import { toUnixPath } from './files';
import { Minimatch } from 'minimatch';
import { type Filesystem, createFindUp } from './find-up';

export const PACKAGE_JSON = 'package.json';
export const parsePackageJson = (_filename: string, contents: string | null) =>
contents ? (JSON.parse(contents) as PackageJson) : {};

/**
* The {@link FindUp} instance dedicated to retrieving `package.json` files
*/
const findPackageJsons = createFindUp(PACKAGE_JSON);

const DefinitelyTyped = '@types/';

/**
Expand Down Expand Up @@ -191,46 +197,19 @@ function addDependency(result: Set<string | Minimatch>, dependency: string, isGl
export const getManifests = (
fileName: string,
workingDirectory: string,
fileSystem: {
readdirSync: (path: string) => Array<string>;
readFileSync: (path: string) => Buffer;
},
fileSystem: Filesystem,
): Array<PackageJson> => {
// if the fileName is not a child of the working directory, we bail
const relativePath = Path.relative(workingDirectory, fileName);

if (relativePath.startsWith('..')) {
return [];
}

const getManifestsAtPath = (path: string): Array<PackageJson> => {
const results: Array<PackageJson> = [];
const entries = fileSystem.readdirSync(path);
const files = findPackageJsons(Path.dirname(fileName), workingDirectory, fileSystem);

for (const entry of entries) {
const entryUnixPath = toUnixPath(entry);
const absoluteEntryPath = Path.join(path, entryUnixPath);
return files.map(file => {
const content = file.content;

if (Path.basename(absoluteEntryPath) === 'package.json') {
const content = fileSystem.readFileSync(absoluteEntryPath);
try {
return JSON.parse(content.toString());
} catch (error) {
console.debug(`Error parsing file ${file.path}: ${error}`);

try {
results.push(JSON.parse(content.toString()));
} catch (error) {
console.debug(`Error parsing file ${absoluteEntryPath}: ${error}`);
}
}
return {};
}

// we stop as soon as the working directory has been reached
if (path !== workingDirectory) {
const parentDirectory = Path.dirname(path);

results.push(...getManifestsAtPath(parentDirectory));
}

return results;
};

return getManifestsAtPath(Path.dirname(fileName));
});
};
Loading

0 comments on commit a5f02df

Please sign in to comment.