8000 feat(typescript-estree): cache project glob resolution by bradzacher · Pull Request #6367 · typescript-eslint/typescript-eslint · GitHub
[go: up one dir, main page]

Skip to content

feat(typescript-estree): cache project glob resolution #6367

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"esquery",
"esrecurse",
"estree",
"globby",
"IDE's",
"IIFE",
"IIFEs",
Expand Down
5 changes: 5 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ module.exports = {
tsconfigRootDir: __dirname,
warnOnUnsupportedTypeScriptVersion: false,
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: false,
cacheLifetime: {
// we pretty well never create/change tsconfig structure - so need to ever evict the cache
// in the rare case that we do - just need to manually restart their IDE.
glob: 'Infinity',
},
},
rules: {
// make sure we're not leveraging any deprecated APIs
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ jspm_packages/
# Editor-specific metadata folders
.vs

# nodejs cpu profiles
*.cpuprofile

.DS_Store
.idea
dist
Expand Down
11 changes: 11 additions & 0 deletions docs/architecture/Parser.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ The following additional configuration options are available by specifying them

```ts
interface ParserOptions {
cacheLifetime?: {
glob?: number | 'Infinity';
};
ecmaFeatures?: {
jsx?: boolean;
globalReturn?: boolean;
Expand All @@ -49,6 +52,14 @@ interface ParserOptions {
}
```

### `cacheLifetime`

This option allows you to granularly control our internal cache expiry lengths.

You can specify the number of seconds as an integer number, or the string 'Infinity' if you never want the cache to expire.

By default cache entries will be evicted after 30 seconds, or will persist indefinitely if the parser infers that it is a single run.

### `ecmaFeatures`

Optional additional options to describe how to parse the raw syntax.
Expand Down
35 changes: 34 additions & 1 deletion docs/architecture/TypeScript-ESTree.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,23 @@ interface ParseAndGenerateServicesOptions extends ParseOptions {
allowAutomaticSingleRunInference?: boolean;

/**
* Path to a file exporting a custom ModuleResolver.
* Granular control of the expiry lifetime of our internal caches.
* You can specify the number of seconds as an integer number, or the string
* 'Infinity' if you never want the cache to expire.
*
* By default cache entries will be evicted after 30 seconds, or will persist
* indefinitely if `allowAutomaticSingleRunInference = true` AND the parser
* infers that it is a single run.
*/
cacheLifetime?: {
/**
* Glob resolution for `parserOptions.project` values.
*/
glob?: number | 'Infinity';
};

/**
* Path to a file exporting a custom `ModuleResolver`.
*/
moduleResolver?: string;
}
Expand Down Expand Up @@ -273,6 +289,23 @@ const { ast, services } = parseAndGenerateServices(code, {
});
```

##### `ModuleResolver`

The `moduleResolver` option allows you to specify the path to a module with a custom module resolver implementation. The module is expected to adhere to the following interface:

```ts
interface ModuleResolver {
version: 1;
resolveModuleNames(
moduleNames: string[],
containingFile: string,
reusedNames: string[] | undefined,
redirectedReference: ts.ResolvedProjectReference | undefined,
options: ts.CompilerOptions,
): (ts.ResolvedModule | undefined)[];
}
``` 6D40

#### `parseWithNodeMaps(code, options)`

Parses the given string of code with the options provided and returns both the ESTree-compatible AST as well as the node maps.
Expand Down
13 changes: 12 additions & 1 deletion packages/types/src/parser-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Program } from 'typescript';
import type { Lib } from './lib';

type DebugLevel = boolean | ('typescript-eslint' | 'eslint' | 'typescript')[];
type CacheDurationSeconds = number | 'Infinity';

type EcmaVersion =
| 3
Expand Down Expand Up @@ -59,7 +60,17 @@ interface ParserOptions {
tsconfigRootDir?: string;
warnOnUnsupportedTypeScriptVersion?: boolean;
moduleResolver?: string;
cacheLifetime?: {
glob?: CacheDurationSeconds;
};

[additionalProperties: string]: unknown;
}

export { DebugLevel, EcmaVersion, ParserOptions, SourceType };
export {
CacheDurationSeconds,
DebugLevel,
EcmaVersion,
ParserOptions,
SourceType,
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { CanonicalPath } from './shared';
import {
canonicalDirname,
createDefaultCompilerOptionsFromExtra,
createHash,
getCanonicalFileName,
getModuleResolver,
} from './shared';
Expand Down Expand Up @@ -105,19 +106,6 @@ function diagnosticReporter(diagnostic: ts.Diagnostic): void {
);
}

/**
* Hash content for compare content.
* @param content hashed contend
* @returns hashed result
*/
function createHash(content: string): string {
// No ts.sys in browser environments.
if (ts.sys?.createHash) {
return ts.sys.createHash(content);
}
return content;
}

function updateCachedFileList(
tsconfigPath: CanonicalPath,
program: ts.Program,
Expand Down
14 changes: 14 additions & 0 deletions packages/typescript-estree/src/create-program/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,26 @@ function getModuleResolver(moduleResolverPath: string): ModuleResolver {
return moduleResolver;
}

/**
* Hash content for compare content.
* @param content hashed contend
* @returns hashed result
*/
function createHash(content: string): string {
// No ts.sys in browser environments.
if (ts.sys?.createHash) {
return ts.sys.createHash(content);
}
return content;
}

export {
ASTAndProgram,
CORE_COMPILER_OPTIONS,
canonicalDirname,
CanonicalPath,
createDefaultCompilerOptionsFromExtra,
createHash,
ensureAbsolutePath,
getCanonicalFileName,
getAstFromProgram,
Expand Down
69 changes: 69 additions & 0 deletions packages/typescript-estree/src/parseSettings/ExpiringCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { CacheDurationSeconds } from '@typescript-eslint/types';

export const DEFAULT_TSCONFIG_CACHE_DURATION_SECONDS = 30;
const ZERO_HR_TIME: [number, number] = [0, 0];

/**
* A map with key-level expiration.
*/
export class ExpiringCache<TKey, TValue> {
readonly #cacheDurationSeconds: CacheDurationSeconds;
/**
* The mapping of path-like string to 10000 the resolved TSConfig(s)
*/
protected readonly map = new Map<
TKey,
Readonly<{
value: TValue;
lastSeen: [number, number];
}>
>();

constructor(cacheDurationSeconds: CacheDurationSeconds) {
this.#cacheDurationSeconds = cacheDurationSeconds;
}

set(key: TKey, value: TValue): this {
this.map.set(key, {
value,
lastSeen:
this.#cacheDurationSeconds === 'Infinity'
? // no need to waste time calculating the hrtime in infinity mode as there's no expiry
ZERO_HR_TIME
: process.hrtime(),
});
return this;
}

get(key: TKey): TValue | undefined {
const entry = this.map.get(key);
if (entry?.value != null) {
if (this.#cacheDurationSeconds === 'Infinity') {
return entry.value;
}

const ageSeconds = process.hrtime(entry.lastSeen)[0];
if (ageSeconds < this.#cacheDurationSeconds) {
// cache hit woo!
return entry.value;
} else {
// key has expired - clean it up to free up memory
this.cleanupKey(key);
}
}
// no hit :'(
return undefined;
}

protected cleanupKey(key: TKey): void {
this.map.delete(key);
}

get size(): number {
return this.map.size;
}

clear(): void {
this.map.clear();
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import debug from 'debug';
import { sync as globSync } from 'globby';
import isGlob from 'is-glob';

import type { CanonicalPath } from '../create-program/shared';
import {
ensureAbsolutePath,
getCanonicalFileName,
} from '../create-program/shared';
import { ensureAbsolutePath } from '../create-program/shared';
import type { TSESTreeOptions } from '../parser-options';
import type { MutableParseSettings } from './index';
import { inferSingleRun } from './inferSingleRun';
import { resolveProjectList } from './resolveProjectList';
import { warnAboutTSVersion } from './warnAboutTSVersion';

const log = debug(
Expand Down Expand Up @@ -98,23 +93,13 @@ export function createParseSettings(

// Providing a program overrides project resolution
if (!parseSettings.programs) {
const projectFolderIgnoreList = (
options.projectFolderIgnoreList ?? ['**/node_modules/**']
)
.reduce<string[]>((acc, folder) => {
if (typeof folder === 'string') {
acc.push(folder);
}
return acc;
}, [])
// prefix with a ! for not match glob
.map(folder => (folder.startsWith('!') ? folder : `!${folder}`));

parseSettings.projects = prepareAndTransformProjects(
tsconfigRootDir,
options.project,
projectFolderIgnoreList,
);
parseSettings.projects = resolveProjectList({
cacheLifetime: options.cacheLifetime,
project: options.project,
projectFolderIgnoreList: options.projectFolderIgnoreList,
singleRun: parseSettings.singleRun,
tsconfigRootDir: tsconfigRootDir,
});
}

warnAboutTSVersion(parseSettings);
Expand Down Expand Up @@ -144,58 +129,3 @@ function enforceString(code: unknown): string {
function getFileName(jsx?: boolean): string {
return jsx ? 'estree.tsx' : 'estree.ts';
}

function getTsconfigPath(
tsconfigPath: string,
tsconfigRootDir: string,
): CanonicalPath {
return getCanonicalFileName(
ensureAbsolutePath(tsconfigPath, tsconfigRootDir),
);
}

/**
* Normalizes, sanitizes, resolves and filters the provided project paths
*/
function prepareAndTransformProjects(
tsconfigRootDir: string,
projectsInput: string | string[] | undefined,
ignoreListInput: string[],
): CanonicalPath[] {
const sanitizedProjects: string[] = [];

// Normalize and sanitize the project paths
if (typeof projectsInput === 'string') {
sanitizedProjects.push(projectsInput);
} else if (Array.isArray(projectsInput)) {
for (const project of projectsInput) {
if (typeof project === 'string') {
sanitizedProjects.push(project);
}
}
}

if (sanitizedProjects.length === 0) {
return [];
}

// Transform glob patterns into paths
const nonGlobProjects = sanitizedProjects.filter(project => !isGlob(project));
const globProjects = sanitizedProjects.filter(project => isGlob(project));
const uniqueCanonicalProjectPaths = new Set(
nonGlobProjects
.concat(
globSync([...globProjects, ...ignoreListInput], {
cwd: tsconfigRootDir,
}),
)
.map(project => getTsconfigPath(project, tsconfigRootDir)),
);

log(
'parserOptions.project (excluding ignored) matched projects: %s',
uniqueCanonicalProjectPaths,
);

return Array.from(uniqueCanonicalProjectPaths);
}
2 changes: 1 addition & 1 deletion packages/typescript-estree/src/parseSettings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export interface MutableParseSettings {
/**
* Normalized paths to provided project paths.
*/
projects: CanonicalPath[];
projects: readonly CanonicalPath[];

/**
* Whether to add the `range` property to AST nodes.
Expand Down
Loading
0