Skip to content

Commit 7fc062a

Browse files
authored
feat: remove partial type-information program (#6066)
1 parent dde6861 commit 7fc062a

32 files changed

+329
-3087
lines changed

packages/eslint-plugin/src/rules/consistent-type-exports.ts

+24-29
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import type {
2-
ParserServices,
3-
TSESLint,
4-
TSESTree,
5-
} from '@typescript-eslint/utils';
1+
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
62
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
73
import { SymbolFlags } from 'typescript';
84

@@ -75,6 +71,28 @@ export default util.createRule<Options, MessageIds>({
7571
const sourceExportsMap: { [key: string]: SourceExports } = {};
7672
const parserServices = util.getParserServices(context);
7773

74+
/**
75+
* Helper for identifying if an export specifier resolves to a
76+
* JavaScript value or a TypeScript type.
77+
*
78+
* @returns True/false if is a type or not, or undefined if the specifier
79+
* can't be resolved.
80+
*/
81+
function isSpecifierTypeBased(
82+
specifier: TSESTree.ExportSpecifier,
83+
): boolean | undefined {
84+
const checker = parserServices.program.getTypeChecker();
85+
const node = parserServices.esTreeNodeToTSNodeMap.get(specifier.exported);
86+
const symbol = checker.getSymbolAtLocation(node);
87+
const aliasedSymbol = checker.getAliasedSymbol(symbol!);
88+
89+
if (!aliasedSymbol || aliasedSymbol.escapedName === 'unknown') {
90+
return undefined;
91+
}
92+
93+
return !(aliasedSymbol.flags & SymbolFlags.Value);
94+
}
95+
7896
return {
7997
ExportNamedDeclaration(node: TSESTree.ExportNamedDeclaration): void {
8098
// Coerce the source into a string for use as a lookup entry.
@@ -112,7 +130,7 @@ export default util.createRule<Options, MessageIds>({
112130
continue;
113131
}
114132

115-
const isTypeBased = isSpecifierTypeBased(parserServices, specifier);
133+
const isTypeBased = isSpecifierTypeBased(specifier);
116134

117135
if (isTypeBased === true) {
118136
typeBasedSpecifiers.push(specifier);
@@ -199,29 +217,6 @@ export default util.createRule<Options, MessageIds>({
199217
},
200218
});
201219

202-
/**
203-
* Helper for identifying if an export specifier resolves to a
204-
* JavaScript value or a TypeScript type.
205-
*
206-
* @returns True/false if is a type or not, or undefined if the specifier
207-
* can't be resolved.
208-
*/
209-
function isSpecifierTypeBased(
210-
parserServices: ParserServices,
211-
specifier: TSESTree.ExportSpecifier,
212-
): boolean | undefined {
213-
const checker = parserServices.program.getTypeChecker();
214-
const node = parserServices.esTreeNodeToTSNodeMap.get(specifier.exported);
215-
const symbol = checker.getSymbolAtLocation(node);
216-
const aliasedSymbol = checker.getAliasedSymbol(symbol!);
217-
218-
if (!aliasedSymbol || aliasedSymbol.escapedName === 'unknown') {
219-
return undefined;
220-
}
221-
222-
return !(aliasedSymbol.flags & SymbolFlags.Value);
223-
}
224-
225220
/**
226221
* Inserts "type" into an export.
227222
*

packages/eslint-plugin/src/rules/naming-convention.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,8 @@ export default util.createRule<Options, MessageIds>({
9090

9191
const validators = parseOptions(context);
9292

93-
// getParserServices(context, false) -- dirty hack to work around the docs checker test...
94-
const compilerOptions = util
95-
.getParserServices(context, true)
96-
.program.getCompilerOptions();
93+
const compilerOptions =
94+
util.getParserServices(context, true).program?.getCompilerOptions() ?? {};
9795
function handleMember(
9896
validator: ValidatorFunction | null,
9997
node:

packages/eslint-plugin/tests/docs.test.ts

+11
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ describe('Validating rule docs', () => {
120120
});
121121

122122
describe('Validating rule metadata', () => {
123+
const rulesThatRequireTypeInformationInAWayThatsHardToDetect = new Set([
124+
// the core rule file doesn't use type information, instead it's used in `src/rules/naming-convention-utils/validator.ts`
125+
'naming-convention',
126+
]);
123127
function requiresFullTypeInformation(content: string): boolean {
124128
return /getParserServices(\(\s*[^,\s)]+)\s*(,\s*false\s*)?\)/.test(content);
125129
}
@@ -135,6 +139,13 @@ describe('Validating rule metadata', () => {
135139
});
136140

137141
it('`requiresTypeChecking` should be set if the rule uses type information', () => {
142+
if (
143+
rulesThatRequireTypeInformationInAWayThatsHardToDetect.has(ruleName)
144+
) {
145+
expect(true).toEqual(rule.meta.docs?.requiresTypeChecking ?? false);
146+
return;
147+
}
148+
138149
// quick-and-dirty check to see if it uses parserServices
139150
// not perfect but should be good enough
140151
const ruleFileContents = fs.readFileSync(

packages/parser/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export { parse, parseForESLint, ParserOptions } from './parser';
22
export {
33
ParserServices,
4+
ParserServicesWithTypeInformation,
5+
ParserServicesWithoutTypeInformation,
46
clearCaches,
57
createProgram,
68
} from '@typescript-eslint/typescript-estree';

packages/parser/src/parser.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ function parseForESLint(
128128
ast.sourceType = options.sourceType;
129129

130130
let emitDecoratorMetadata = options.emitDecoratorMetadata === true;
131-
if (services.hasFullTypeInformation) {
131+
if (services.program) {
132132
// automatically apply the options configured for the program
133133
const compilerOptions = services.program.getCompilerOptions();
134134
if (analyzeOptions.lib == null) {

packages/type-utils/tests/isTypeReadonly.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type ReadonlynessOptions,
88
isTypeReadonly,
99
} from '../src/isTypeReadonly';
10+
import { expectToHaveParserServices } from './test-utils/expectToHaveParserServices';
1011

1112
describe('isTypeReadonly', () => {
1213
const rootDir = path.join(__dirname, 'fixtures');
@@ -21,6 +22,7 @@ describe('isTypeReadonly', () => {
2122
filePath: path.join(rootDir, 'file.ts'),
2223
tsconfigRootDir: rootDir,
2324
});
25+
expectToHaveParserServices(services);
2426
const checker = services.program.getTypeChecker();
2527
const esTreeNodeToTSNodeMap = services.esTreeNodeToTSNodeMap;
2628

packages/type-utils/tests/isUnsafeAssignment.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from 'path';
44
import type * as ts from 'typescript';
55

66
import { isUnsafeAssignment } from '../src/isUnsafeAssignment';
7+
import { expectToHaveParserServices } from './test-utils/expectToHaveParserServices';
78

89
describe('isUnsafeAssignment', () => {
910
const rootDir = path.join(__dirname, 'fixtures');
@@ -19,6 +20,7 @@ describe('isUnsafeAssignment', () => {
1920
filePath: path.join(rootDir, 'file.ts'),
2021
tsconfigRootDir: rootDir,
2122
});
23+
expectToHaveParserServices(services);
2224
const checker = services.program.getTypeChecker();
2325
const esTreeNodeToTSNodeMap = services.esTreeNodeToTSNodeMap;
2426

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type {
2+
ParserServices,
3+
ParserServicesWithTypeInformation,
4+
} from '@typescript-eslint/typescript-estree';
5+
6+
export function expectToHaveParserServices(
7+
services: ParserServices | null | undefined,
8+
): asserts services is ParserServicesWithTypeInformation {
9+
expect(services?.program).toBeDefined();
10+
expect(services?.esTreeNodeToTSNodeMap).toBeDefined();
11+
expect(services?.tsNodeToESTreeNodeMap).toBeDefined();
12+
}

packages/typescript-estree/jest.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
module.exports = {
66
...require('../../jest.config.base.js'),
77
testRegex: [
8-
'./tests/lib/.*\\.ts$',
8+
'./tests/lib/.*\\.test\\.ts$',
99
'./tests/ast-alignment/spec\\.ts$',
1010
'./tests/[^/]+\\.test\\.ts$',
1111
],

packages/typescript-estree/src/create-program/createDefaultProgram.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from 'path';
33
import * as ts from 'typescript';
44

55
import type { ParseSettings } from '../parseSettings';
6-
import type { ASTAndProgram } from './shared';
6+
import type { ASTAndDefiniteProgram } from './shared';
77
import {
88
createDefaultCompilerOptionsFromExtra,
99
getModuleResolver,
@@ -20,7 +20,7 @@ const log = debug('typescript-eslint:typescript-estree:createDefaultProgram');
2020
*/
2121
function createDefaultProgram(
2222
parseSettings: ParseSettings,
23-
): ASTAndProgram | undefined {
23+
): ASTAndDefiniteProgram | undefined {
2424
log(
2525
'Getting default program for: %s',
2626
parseSettings.filePath || 'unnamed file',

packages/typescript-estree/src/create-program/createIsolatedProgram.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as ts from 'typescript';
33

44
import type { ParseSettings } from '../parseSettings';
55
import { getScriptKind } from './getScriptKind';
6-
import type { ASTAndProgram } from './shared';
6+
import type { ASTAndDefiniteProgram } from './shared';
77
import { createDefaultCompilerOptionsFromExtra } from './shared';
88

99
const log = debug('typescript-eslint:typescript-estree:createIsolatedProgram');
@@ -12,7 +12,9 @@ const log = debug('typescript-eslint:typescript-estree:createIsolatedProgram');
1212
* @param code The code of the file being linted
1313
* @returns Returns a new source file and program corresponding to the linted code
1414
*/
15-
function createIsolatedProgram(parseSettings: ParseSettings): ASTAndProgram {
15+
function createIsolatedProgram(
16+
parseSettings: ParseSettings,
17+
): ASTAndDefiniteProgram {
1618
log(
1719
'Getting isolated program in %s mode for: %s',
1820
parseSettings.jsx ? 'TSX' : 'TS',

packages/typescript-estree/src/create-program/createProjectProgram.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as ts from 'typescript';
55
import { firstDefined } from '../node-utils';
66
import type { ParseSettings } from '../parseSettings';
77
import { getWatchProgramsForProjects } from './getWatchProgramsForProjects';
8-
import type { ASTAndProgram } from './shared';
8+
import type { ASTAndDefiniteProgram } from './shared';
99
import { getAstFromProgram } from './shared';
1010

1111
const log = debug('typescript-eslint:typescript-estree:createProjectProgram');
@@ -27,7 +27,7 @@ const DEFAULT_EXTRA_FILE_EXTENSIONS = [
2727
*/
2828
function createProjectProgram(
2929
parseSettings: ParseSettings,
30-
): ASTAndProgram | undefined {
30+
): ASTAndDefiniteProgram | undefined {
3131
log('Creating project program for: %s', parseSettings.filePath);
3232

3333
const programsForProjects = getWatchProgramsForProjects(parseSettings);

packages/typescript-estree/src/create-program/createSourceFile.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as ts from 'typescript';
44
import type { ParseSettings } from '../parseSettings';
55
import { isSourceFile } from '../source-files';
66
import { getScriptKind } from './getScriptKind';
7+
import type { ASTAndNoProgram } from './shared';
78

89
const log = debug('typescript-eslint:typescript-estree:createSourceFile');
910

@@ -25,4 +26,11 @@ function createSourceFile(parseSettings: ParseSettings): ts.SourceFile {
2526
);
2627
}
2728

28-
export { createSourceFile };
29+
function createNoProgram(parseSettings: ParseSettings): ASTAndNoProgram {
30+
return {
31+
ast: createSourceFile(parseSettings),
32+
program: null,
33+
};
34+
}
35+
36+
export { createSourceFile, createNoProgram };

packages/typescript-estree/src/create-program/shared.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@ import * as ts from 'typescript';
55
import type { ModuleResolver } from '../parser-options';
66
import type { ParseSettings } from '../parseSettings';
77

8-
interface ASTAndProgram {
8+
interface ASTAndNoProgram {
9+
ast: ts.SourceFile;
10+
program: null;
11+
}
12+
interface ASTAndDefiniteProgram {
913
ast: ts.SourceFile;
1014
program: ts.Program;
1115
}
16+
type ASTAndProgram = ASTAndNoProgram | ASTAndDefiniteProgram;
1217

1318
/**
1419
* Compiler options required to avoid critical functionality issues
@@ -94,7 +99,7 @@ function getExtension(fileName: string | undefined): string | null {
9499
function getAstFromProgram(
95100
currentProgram: Program,
96101
parseSettings: ParseSettings,
97-
): ASTAndProgram | undefined {
102+
): ASTAndDefiniteProgram | undefined {
98103
const ast = currentProgram.getSourceFile(parseSettings.filePath);
99104

100105
// working around https://github.com/typescript-eslint/typescript-eslint/issues/1573
@@ -125,6 +130,8 @@ function getModuleResolver(moduleResolverPath: string): ModuleResolver {
125130
}
126131

127132
export {
133+
ASTAndDefiniteProgram,
134+
ASTAndNoProgram,
128135
ASTAndProgram,
129136
CORE_COMPILER_OPTIONS,
130137
canonicalDirname,

packages/typescript-estree/src/create-program/useProvidedPrograms.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,21 @@ import * as path from 'path';
44
import * as ts from 'typescript';
55

66
import type { ParseSettings } from '../parseSettings';
7-
import type { ASTAndProgram } from './shared';
7+
import type { ASTAndDefiniteProgram } from './shared';
88
import { CORE_COMPILER_OPTIONS, getAstFromProgram } from './shared';
99

1010
const log = debug('typescript-eslint:typescript-estree:useProvidedProgram');
1111

1212
function useProvidedPrograms(
1313
programInstances: Iterable<ts.Program>,
1414
parseSettings: ParseSettings,
15-
): ASTAndProgram | undefined {
15+
): ASTAndDefiniteProgram | undefined {
1616
log(
1717
'Retrieving ast for %s from provided program instance(s)',
1818
parseSettings.filePath,
1919
);
2020

21-
let astAndProgram: ASTAndProgram | undefined;
21+
let astAndProgram: ASTAndDefiniteProgram | undefined;
2222
for (const programInstance of programInstances) {
2323
astAndProgram = getAstFromProgram(programInstance, parseSettings);
2424
// Stop at the first applicable program instance

packages/typescript-estree/src/index.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ export {
77
ParseWithNodeMapsResult,
88
clearProgramCache,
99
} from './parser';
10-
export { ParserServices, TSESTreeOptions } from './parser-options';
10+
export {
11+
ParserServices,
12+
ParserServicesWithTypeInformation,
13+
ParserServicesWithoutTypeInformation,
14+
TSESTreeOptions,
15+
} from './parser-options';
1116
export { simpleTraverse } from './simple-traverse';
1217
export * from './ts-estree';
1318
export { clearWatchCaches as clearCaches } from './create-program/getWatchProgramsForProjects';

packages/typescript-estree/src/parser-options.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,21 @@ export interface ParserWeakMapESTreeToTSNode<
182182
has(key: unknown): boolean;
183183
}
184184

185-
export interface ParserServices {
186-
program: ts.Program;
185+
export interface ParserServicesNodeMaps {
187186
esTreeNodeToTSNodeMap: ParserWeakMapESTreeToTSNode;
188187
tsNodeToESTreeNodeMap: ParserWeakMap<TSNode | TSToken, TSESTree.Node>;
189-
hasFullTypeInformation: boolean;
190188
}
189+
export interface ParserServicesWithTypeInformation
190+
extends ParserServicesNodeMaps {
191+
program: ts.Program;
192+
}
193+
export interface ParserServicesWithoutTypeInformation
194+
extends ParserServicesNodeMaps {
195+
program: null;
196+
}
197+
export type ParserServices =
198+
| ParserServicesWithTypeInformation
199+
| ParserServicesWithoutTypeInformation;
191200

192201
export interface ModuleResolver {
193202
version: 1;

0 commit comments

Comments
 (0)