Skip to content

feat(typescript-estree): allow specifying project: true #6084

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
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
efec873
feat(typescript-estree): allow specifying project: true
JoshuaKGoldberg Nov 25, 2022
4927c39
Also fix unchanged file for lint I guess
JoshuaKGoldberg Nov 25, 2022
d01f2bf
Added docs
JoshuaKGoldberg Nov 25, 2022
4bf121e
More tsconfigRootDir: __dirname removal
JoshuaKGoldberg Nov 25, 2022
db656f5
Added some unit tests
JoshuaKGoldberg Nov 25, 2022
003d993
you don't say
JoshuaKGoldberg Nov 25, 2022
eb34a95
Undo createWatchProgram.ts
JoshuaKGoldberg Nov 25, 2022
3a428c1
Merge branch 'main' into parser-options-project-true
JoshuaKGoldberg Nov 28, 2022
9ae3ce6
Added parse tests
JoshuaKGoldberg Nov 30, 2022
7cf6b80
Merge branch 'main' into parser-options-project-true
JoshuaKGoldberg Nov 30, 2022
3682aaf
Merge branch 'main'
JoshuaKGoldberg Dec 3, 2022
b024ad2
Fixed monorepos link
JoshuaKGoldberg Dec 3, 2022
6b16b04
Cache under all directories
JoshuaKGoldberg Dec 8, 2022
264a279
Added another test, just to be sure
JoshuaKGoldberg Dec 8, 2022
725603e
Merge branch 'main' into parser-options-project-true
JoshuaKGoldberg Dec 8, 2022
3c4850b
lint fix
JoshuaKGoldberg Dec 10, 2022
16e52d5
Add back tsconfigRootDir
JoshuaKGoldberg Dec 13, 2022
2be55de
Apply suggestions from code review
JoshuaKGoldberg Jan 23, 2023
fb6465c
Back to string |
JoshuaKGoldberg Jan 25, 2023
e6df49a
Merge branch main and use ExpiringCache and idk what else git is hard
JoshuaKGoldberg Feb 4, 2023
edc11e5
Fix website build
JoshuaKGoldberg Feb 4, 2023
315bcde
Fix the build
JoshuaKGoldberg Feb 4, 2023
040a56a
A global tsconfigMatchCache, with a test
JoshuaKGoldberg Feb 9, 2023
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
9 changes: 8 additions & 1 deletion docs/architecture/Parser.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ interface ParserOptions {
lib?: string[];
moduleResolver?: string;
program?: import('typescript').Program;
project?: string | string[];
project?: string | string[] | true;
projectFolderIgnoreList?: string[];
tsconfigRootDir?: string;
warnOnUnsupportedTypeScriptVersion?: boolean;
Expand Down Expand Up @@ -188,6 +188,9 @@ This option allows you to provide a path to your project's `tsconfig.json`. **Th
- Accepted values:

```js
// find the tsconfig.json nearest each source file
project: true,

// path
project: './tsconfig.json';

Expand All @@ -198,6 +201,10 @@ This option allows you to provide a path to your project's `tsconfig.json`. **Th
project: ['./packages/**/tsconfig.json', './separate-package/tsconfig.json'];
```

- If `true`, each source file's parse will find the nearest `tsconfig.json` file to that source file.

- This is done by checking that source file's directory tree for the nearest `tsconfig.json`.

- If you use project references, TypeScript will not automatically use project references to resolve files. This means that you will have to add each referenced tsconfig to the `project` field either separately, or via a glob.

- Note that using wide globs `**` in your `parserOptions.project` may cause performance implications. Instead of globs that use `**` to recursively check all folders, prefer paths that use a single `*` at a time. For more info see [#2611](https://github.com/typescript-eslint/typescript-eslint/issues/2611).
Expand Down
5 changes: 3 additions & 2 deletions docs/architecture/TypeScript-ESTree.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,11 @@ interface ParseAndGenerateServicesOptions extends ParseOptions {
preserveNodeMaps?: boolean;

/**
* Absolute (or relative to `tsconfigRootDir`) paths to the tsconfig(s).
* Absolute (or relative to `tsconfigRootDir`) paths to the tsconfig(s),
* or `true` to find the nearest tsconfig.json to the file.
* If this is provided, type information will be returned.
*/
project?: string | string[];
project?: string | string[] | true;

/**
* If you provide a glob (or globs) to the project option, you can use this option to ignore certain folders from
Expand Down
27 changes: 26 additions & 1 deletion docs/linting/Typed_Linting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = {
parser: '@typescript-eslint/parser',
// Added lines start
parserOptions: {
project: ['./tsconfig.json'],
project: true,
tsconfigRootDir: __dirname,
},
// Added lines end
Expand All @@ -36,6 +36,31 @@ In more detail:
With that done, run the same lint command you ran before.
You may see new rules reporting errors based on type information!

## Specifying TSConfigs

The `parserOptions.project` option can be turned on with either:

- `true`: to always use `tsconfig.json`s nearest to source files
- `string | string[]`: any number of glob paths to match TSConfig files relative to the

For example, if you use a specific `tsconfig.eslint.json` for linting, you'd specify:

```js title=".eslintrc.js"
module.exports = {
// ...
parserOptions: {
project: './tsconfig.eslint.json',
},
// ...
};
```

See [the `@typescript-eslint/parser` docs for more details](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/README.md#parseroptionsproject).

:::note
If your project is a multi-package monorepo, see [our docs on configuring a monorepo](./typed-linting/Monorepos.mdx).
:::

## FAQs

### How is performance?
Expand Down
2 changes: 1 addition & 1 deletion docs/linting/typed-linting/Monorepos.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
// Remove this line
project: ['./tsconfig.json'],
project: true,
// Add this line
project: ['./tsconfig.eslint.json', './packages/*/tsconfig.json'],
},
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/parser-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ interface ParserOptions {
filePath?: string;
loc?: boolean;
program?: Program;
project?: string | string[];
project?: string | string[] | true;
projectFolderIgnoreList?: (string | RegExp)[];
range?: boolean;
sourceType?: SourceType;
Expand Down
29 changes: 12 additions & 17 deletions packages/typescript-estree/src/parseSettings/ExpiringCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ import type { CacheDurationSeconds } from '@typescript-eslint/types';
export const DEFAULT_TSCONFIG_CACHE_DURATION_SECONDS = 30;
const ZERO_HR_TIME: [number, number] = [0, 0];

export interface CacheLike<Key, Value> {
get(key: Key): Value | void;
set(key: Key, value: Value): this;
}

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

readonly #map = new Map<
TKey,
Readonly<{
value: TValue;
Expand All @@ -24,7 +27,7 @@ export class ExpiringCache<TKey, TValue> {
}

set(key: TKey, value: TValue): this {
this.map.set(key, {
this.#map.set(key, {
value,
lastSeen:
this.#cacheDurationSeconds === 'Infinity'
Expand All @@ -36,7 +39,7 @@ export class ExpiringCache<TKey, TValue> {
}

get(key: TKey): TValue | undefined {
const entry = this.map.get(key);
const entry = this.#map.get(key);
if (entry?.value != null) {
if (this.#cacheDurationSeconds === 'Infinity') {
return entry.value;
Expand All @@ -48,22 +51,14 @@ export class ExpiringCache<TKey, TValue> {
return entry.value;
} else {
// key has expired - clean it up to free up memory
this.cleanupKey(key);
this.#map.delete(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();
this.#map.clear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import debug from 'debug';

import { ensureAbsolutePath } from '../create-program/shared';
import type { TSESTreeOptions } from '../parser-options';
import {
DEFAULT_TSCONFIG_CACHE_DURATION_SECONDS,
ExpiringCache,
} from './ExpiringCache';
import { getProjectConfigFiles } from './getProjectConfigFiles';
import type { MutableParseSettings } from './index';
import { inferSingleRun } from './inferSingleRun';
import { resolveProjectList } from './resolveProjectList';
Expand All @@ -11,10 +16,13 @@ const log = debug(
'typescript-eslint:typescript-estree:parser:parseSettings:createParseSettings',
);

let TSCONFIG_MATCH_CACHE: ExpiringCache<string, string> | null;

export function createParseSettings(
code: string,
options: Partial<TSESTreeOptions> = {},
): MutableParseSettings {
const singleRun = inferSingleRun(options);
const tsconfigRootDir =
typeof options.tsconfigRootDir === 'string'
? options.tsconfigRootDir
Expand Down Expand Up @@ -58,8 +66,14 @@ export function createParseSettings(
programs: Array.isArray(options.programs) ? options.programs : null,
projects: [],
range: options.range === true,
singleRun: inferSingleRun(options),
singleRun,
tokens: options.tokens === true ? [] : null,
tsconfigMatchCache: (TSCONFIG_MATCH_CACHE ??= new ExpiringCache(
singleRun
? 'Infinity'
: options.cacheLifetime?.glob ??
DEFAULT_TSCONFIG_CACHE_DURATION_SECONDS,
)),
tsconfigRootDir,
};

Expand Down Expand Up @@ -95,7 +109,7 @@ export function createParseSettings(
if (!parseSettings.programs) {
parseSettings.projects = resolveProjectList({
cacheLifetime: options.cacheLifetime,
project: options.project,
project: getProjectConfigFiles(parseSettings, options.project),
projectFolderIgnoreList: options.projectFolderIgnoreList,
singleRun: parseSettings.singleRun,
tsconfigRootDir: tsconfigRootDir,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import debug from 'debug';
import * as fs from 'fs';
import * as path from 'path';

import type { ParseSettings } from '.';

const log = debug('typescript-eslint:typescript-estree:getProjectConfigFiles');

/**
* Checks for a matching TSConfig to a file including its parent directories,
* permanently caching results under each directory it checks.
*
* @remarks
* We don't (yet!) have a way to attach file watchers on disk, but still need to
* cache file checks for rapid subsequent calls to fs.existsSync. See discussion
* in https://github.com/typescript-eslint/typescript-eslint/issues/101.
*/
export function getProjectConfigFiles(
parseSettings: Pick<
ParseSettings,
'filePath' | 'tsconfigMatchCache' | 'tsconfigRootDir'
>,
project: string | string[] | true | undefined,
): string[] | undefined {
if (project !== true) {
return project === undefined || Array.isArray(project)
? project
: [project];
}

log('Looking for tsconfig.json at or above file: %s', parseSettings.filePath);
let directory = path.dirname(parseSettings.filePath);
const checkedDirectories = [directory];

do {
log('Checking tsconfig.json path: %s', directory);
const tsconfigPath = path.join(directory, 'tsconfig.json');
const cached =
parseSettings.tsconfigMatchCache.get(directory) ??
(fs.existsSync(tsconfigPath) && tsconfigPath);

if (cached) {
for (const directory of checkedDirectories) {
parseSettings.tsconfigMatchCache.set(directory, cached);
}
return [cached];
}

directory = path.dirname(directory);
checkedDirectories.push(directory);
} while (
directory.length > 1 &&
directory.length >= parseSettings.tsconfigRootDir.length
);

throw new Error(
`project was set to \`true\` but couldn't find any tsconfig.json relative to '${parseSettings.filePath}' within '${parseSettings.tsconfigRootDir}'.`,
);
}
6 changes: 6 additions & 0 deletions packages/typescript-estree/src/parseSettings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type * as ts from 'typescript';

import type { CanonicalPath } from '../create-program/shared';
import type { TSESTree } from '../ts-estree';
import type { CacheLike } from './ExpiringCache';

type DebugModule = 'typescript-eslint' | 'eslint' | 'typescript';

Expand Down Expand Up @@ -115,6 +116,11 @@ export interface MutableParseSettings {
*/
tokens: null | TSESTree.Token[];

/**
* Caches searches for TSConfigs from project directories.
*/
tsconfigMatchCache: CacheLike<string, string>;

/**
* The absolute path to the root directory for all provided `project`s.
*/
Expand Down
5 changes: 3 additions & 2 deletions packages/typescript-estree/src/parser-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,11 @@ interface ParseAndGenerateServicesOptions extends ParseOptions {
preserveNodeMaps?: boolean;

/**
* Absolute (or relative to `tsconfigRootDir`) paths to the tsconfig(s).
* Absolute (or relative to `tsconfigRootDir`) paths to the tsconfig(s),
* or `true` to find the nearest tsconfig.json to the file.
* If this is provided, type information will be returned.
*/
project?: string | string[];
project?: string | string[] | true;

/**
* If you provide a glob (or globs) to the project option, you can use this option to ignore certain folders from
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const b = true;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const b = true;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"include": ["."]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const c = true;
14 changes: 14 additions & 0 deletions packages/typescript-estree/tests/lib/createParseSettings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createParseSettings } from '../../src/parseSettings/createParseSettings';

describe('createParseSettings', () => {
describe('tsconfigMatchCache', () => {
it('reuses the TSConfig match cache when called a subsequent time', () => {
const parseSettings1 = createParseSettings('input.ts');
const parseSettings2 = createParseSettings('input.ts');

expect(parseSettings1.tsconfigMatchCache).toBe(
parseSettings2.tsconfigMatchCache,
);
});
});
});
Loading