Skip to content

Commit

Permalink
feat: remove partial type-information program (#6066)
Browse files Browse the repository at this point in the history
  • Loading branch information
bradzacher committed Jan 23, 2023
1 parent dde6861 commit 7fc062a
Show file tree
Hide file tree
Showing 32 changed files with 329 additions and 3,087 deletions.
53 changes: 24 additions & 29 deletions packages/eslint-plugin/src/rules/consistent-type-exports.ts
@@ -1,8 +1,4 @@
import type {
ParserServices,
TSESLint,
TSESTree,
} from '@typescript-eslint/utils';
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
import { SymbolFlags } from 'typescript';

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

/**
* Helper for identifying if an export specifier resolves to a
* JavaScript value or a TypeScript type.
*
* @returns True/false if is a type or not, or undefined if the specifier
* can't be resolved.
*/
function isSpecifierTypeBased(
specifier: TSESTree.ExportSpecifier,
): boolean | undefined {
const checker = parserServices.program.getTypeChecker();
const node = parserServices.esTreeNodeToTSNodeMap.get(specifier.exported);
const symbol = checker.getSymbolAtLocation(node);
const aliasedSymbol = checker.getAliasedSymbol(symbol!);

if (!aliasedSymbol || aliasedSymbol.escapedName === 'unknown') {
return undefined;
}

return !(aliasedSymbol.flags & SymbolFlags.Value);
}

return {
ExportNamedDeclaration(node: TSESTree.ExportNamedDeclaration): void {
// Coerce the source into a string for use as a lookup entry.
Expand Down Expand Up @@ -112,7 +130,7 @@ export default util.createRule<Options, MessageIds>({
continue;
}

const isTypeBased = isSpecifierTypeBased(parserServices, specifier);
const isTypeBased = isSpecifierTypeBased(specifier);

if (isTypeBased === true) {
typeBasedSpecifiers.push(specifier);
Expand Down Expand Up @@ -199,29 +217,6 @@ export default util.createRule<Options, MessageIds>({
},
});

/**
* Helper for identifying if an export specifier resolves to a
* JavaScript value or a TypeScript type.
*
* @returns True/false if is a type or not, or undefined if the specifier
* can't be resolved.
*/
function isSpecifierTypeBased(
parserServices: ParserServices,
specifier: TSESTree.ExportSpecifier,
): boolean | undefined {
const checker = parserServices.program.getTypeChecker();
const node = parserServices.esTreeNodeToTSNodeMap.get(specifier.exported);
const symbol = checker.getSymbolAtLocation(node);
const aliasedSymbol = checker.getAliasedSymbol(symbol!);

if (!aliasedSymbol || aliasedSymbol.escapedName === 'unknown') {
return undefined;
}

return !(aliasedSymbol.flags & SymbolFlags.Value);
}

/**
* Inserts "type" into an export.
*
Expand Down
6 changes: 2 additions & 4 deletions packages/eslint-plugin/src/rules/naming-convention.ts
Expand Up @@ -90,10 +90,8 @@ export default util.createRule<Options, MessageIds>({

const validators = parseOptions(context);

// getParserServices(context, false) -- dirty hack to work around the docs checker test...
const compilerOptions = util
.getParserServices(context, true)
.program.getCompilerOptions();
const compilerOptions =
util.getParserServices(context, true).program?.getCompilerOptions() ?? {};
function handleMember(
validator: ValidatorFunction | null,
node:
Expand Down
11 changes: 11 additions & 0 deletions packages/eslint-plugin/tests/docs.test.ts
Expand Up @@ -120,6 +120,10 @@ describe('Validating rule docs', () => {
});

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

it('`requiresTypeChecking` should be set if the rule uses type information', () => {
if (
rulesThatRequireTypeInformationInAWayThatsHardToDetect.has(ruleName)
) {
expect(true).toEqual(rule.meta.docs?.requiresTypeChecking ?? false);
return;
}

// quick-and-dirty check to see if it uses parserServices
// not perfect but should be good enough
const ruleFileContents = fs.readFileSync(
Expand Down
2 changes: 2 additions & 0 deletions packages/parser/src/index.ts
@@ -1,6 +1,8 @@
export { parse, parseForESLint, ParserOptions } from './parser';
export {
ParserServices,
ParserServicesWithTypeInformation,
ParserServicesWithoutTypeInformation,
clearCaches,
createProgram,
} from '@typescript-eslint/typescript-estree';
Expand Down
2 changes: 1 addition & 1 deletion packages/parser/src/parser.ts
Expand Up @@ -128,7 +128,7 @@ function parseForESLint(
ast.sourceType = options.sourceType;

let emitDecoratorMetadata = options.emitDecoratorMetadata === true;
if (services.hasFullTypeInformation) {
if (services.program) {
// automatically apply the options configured for the program
const compilerOptions = services.program.getCompilerOptions();
if (analyzeOptions.lib == null) {
Expand Down
2 changes: 2 additions & 0 deletions packages/type-utils/tests/isTypeReadonly.test.ts
Expand Up @@ -7,6 +7,7 @@ import {
type ReadonlynessOptions,
isTypeReadonly,
} from '../src/isTypeReadonly';
import { expectToHaveParserServices } from './test-utils/expectToHaveParserServices';

describe('isTypeReadonly', () => {
const rootDir = path.join(__dirname, 'fixtures');
Expand All @@ -21,6 +22,7 @@ describe('isTypeReadonly', () => {
filePath: path.join(rootDir, 'file.ts'),
tsconfigRootDir: rootDir,
});
expectToHaveParserServices(services);
const checker = services.program.getTypeChecker();
const esTreeNodeToTSNodeMap = services.esTreeNodeToTSNodeMap;

Expand Down
2 changes: 2 additions & 0 deletions packages/type-utils/tests/isUnsafeAssignment.test.ts
Expand Up @@ -4,6 +4,7 @@ import path from 'path';
import type * as ts from 'typescript';

import { isUnsafeAssignment } from '../src/isUnsafeAssignment';
import { expectToHaveParserServices } from './test-utils/expectToHaveParserServices';

describe('isUnsafeAssignment', () => {
const rootDir = path.join(__dirname, 'fixtures');
Expand All @@ -19,6 +20,7 @@ describe('isUnsafeAssignment', () => {
filePath: path.join(rootDir, 'file.ts'),
tsconfigRootDir: rootDir,
});
expectToHaveParserServices(services);
const checker = services.program.getTypeChecker();
const esTreeNodeToTSNodeMap = services.esTreeNodeToTSNodeMap;

Expand Down
12 changes: 12 additions & 0 deletions packages/type-utils/tests/test-utils/expectToHaveParserServices.ts
@@ -0,0 +1,12 @@
import type {
ParserServices,
ParserServicesWithTypeInformation,
} from '@typescript-eslint/typescript-estree';

export function expectToHaveParserServices(
services: ParserServices | null | undefined,
): asserts services is ParserServicesWithTypeInformation {
expect(services?.program).toBeDefined();
expect(services?.esTreeNodeToTSNodeMap).toBeDefined();
expect(services?.tsNodeToESTreeNodeMap).toBeDefined();
}
2 changes: 1 addition & 1 deletion packages/typescript-estree/jest.config.js
Expand Up @@ -5,7 +5,7 @@
module.exports = {
...require('../../jest.config.base.js'),
testRegex: [
'./tests/lib/.*\\.ts$',
'./tests/lib/.*\\.test\\.ts$',
'./tests/ast-alignment/spec\\.ts$',
'./tests/[^/]+\\.test\\.ts$',
],
Expand Down
Expand Up @@ -3,7 +3,7 @@ import path from 'path';
import * as ts from 'typescript';

import type { ParseSettings } from '../parseSettings';
import type { ASTAndProgram } from './shared';
import type { ASTAndDefiniteProgram } from './shared';
import {
createDefaultCompilerOptionsFromExtra,
getModuleResolver,
Expand All @@ -20,7 +20,7 @@ const log = debug('typescript-eslint:typescript-estree:createDefaultProgram');
*/
function createDefaultProgram(
parseSettings: ParseSettings,
): ASTAndProgram | undefined {
): ASTAndDefiniteProgram | undefined {
log(
'Getting default program for: %s',
parseSettings.filePath || 'unnamed file',
Expand Down
Expand Up @@ -3,7 +3,7 @@ import * as ts from 'typescript';

import type { ParseSettings } from '../parseSettings';
import { getScriptKind } from './getScriptKind';
import type { ASTAndProgram } from './shared';
import type { ASTAndDefiniteProgram } from './shared';
import { createDefaultCompilerOptionsFromExtra } from './shared';

const log = debug('typescript-eslint:typescript-estree:createIsolatedProgram');
Expand All @@ -12,7 +12,9 @@ const log = debug('typescript-eslint:typescript-estree:createIsolatedProgram');
* @param code The code of the file being linted
* @returns Returns a new source file and program corresponding to the linted code
*/
function createIsolatedProgram(parseSettings: ParseSettings): ASTAndProgram {
function createIsolatedProgram(
parseSettings: ParseSettings,
): ASTAndDefiniteProgram {
log(
'Getting isolated program in %s mode for: %s',
parseSettings.jsx ? 'TSX' : 'TS',
Expand Down
Expand Up @@ -5,7 +5,7 @@ import * as ts from 'typescript';
import { firstDefined } from '../node-utils';
import type { ParseSettings } from '../parseSettings';
import { getWatchProgramsForProjects } from './getWatchProgramsForProjects';
import type { ASTAndProgram } from './shared';
import type { ASTAndDefiniteProgram } from './shared';
import { getAstFromProgram } from './shared';

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

const programsForProjects = getWatchProgramsForProjects(parseSettings);
Expand Down
Expand Up @@ -4,6 +4,7 @@ import * as ts from 'typescript';
import type { ParseSettings } from '../parseSettings';
import { isSourceFile } from '../source-files';
import { getScriptKind } from './getScriptKind';
import type { ASTAndNoProgram } from './shared';

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

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

export { createSourceFile };
function createNoProgram(parseSettings: ParseSettings): ASTAndNoProgram {
return {
ast: createSourceFile(parseSettings),
program: null,
};
}

export { createSourceFile, createNoProgram };
11 changes: 9 additions & 2 deletions packages/typescript-estree/src/create-program/shared.ts
Expand Up @@ -5,10 +5,15 @@ import * as ts from 'typescript';
import type { ModuleResolver } from '../parser-options';
import type { ParseSettings } from '../parseSettings';

interface ASTAndProgram {
interface ASTAndNoProgram {
ast: ts.SourceFile;
program: null;
}
interface ASTAndDefiniteProgram {
ast: ts.SourceFile;
program: ts.Program;
}
type ASTAndProgram = ASTAndNoProgram | ASTAndDefiniteProgram;

/**
* Compiler options required to avoid critical functionality issues
Expand Down Expand Up @@ -94,7 +99,7 @@ function getExtension(fileName: string | undefined): string | null {
function getAstFromProgram(
currentProgram: Program,
parseSettings: ParseSettings,
): ASTAndProgram | undefined {
): ASTAndDefiniteProgram | undefined {
const ast = currentProgram.getSourceFile(parseSettings.filePath);

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

export {
ASTAndDefiniteProgram,
ASTAndNoProgram,
ASTAndProgram,
CORE_COMPILER_OPTIONS,
canonicalDirname,
Expand Down
Expand Up @@ -4,21 +4,21 @@ import * as path from 'path';
import * as ts from 'typescript';

import type { ParseSettings } from '../parseSettings';
import type { ASTAndProgram } from './shared';
import type { ASTAndDefiniteProgram } from './shared';
import { CORE_COMPILER_OPTIONS, getAstFromProgram } from './shared';

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

function useProvidedPrograms(
programInstances: Iterable<ts.Program>,
parseSettings: ParseSettings,
): ASTAndProgram | undefined {
): ASTAndDefiniteProgram | undefined {
log(
'Retrieving ast for %s from provided program instance(s)',
parseSettings.filePath,
);

let astAndProgram: ASTAndProgram | undefined;
let astAndProgram: ASTAndDefiniteProgram | undefined;
for (const programInstance of programInstances) {
astAndProgram = getAstFromProgram(programInstance, parseSettings);
// Stop at the first applicable program instance
Expand Down
7 changes: 6 additions & 1 deletion packages/typescript-estree/src/index.ts
Expand Up @@ -7,7 +7,12 @@ export {
ParseWithNodeMapsResult,
clearProgramCache,
} from './parser';
export { ParserServices, TSESTreeOptions } from './parser-options';
export {
ParserServices,
ParserServicesWithTypeInformation,
ParserServicesWithoutTypeInformation,
TSESTreeOptions,
} from './parser-options';
export { simpleTraverse } from './simple-traverse';
export * from './ts-estree';
export { clearWatchCaches as clearCaches } from './create-program/getWatchProgramsForProjects';
Expand Down
15 changes: 12 additions & 3 deletions packages/typescript-estree/src/parser-options.ts
Expand Up @@ -182,12 +182,21 @@ export interface ParserWeakMapESTreeToTSNode<
has(key: unknown): boolean;
}

export interface ParserServices {
program: ts.Program;
export interface ParserServicesNodeMaps {
esTreeNodeToTSNodeMap: ParserWeakMapESTreeToTSNode;
tsNodeToESTreeNodeMap: ParserWeakMap<TSNode | TSToken, TSESTree.Node>;
hasFullTypeInformation: boolean;
}
export interface ParserServicesWithTypeInformation
extends ParserServicesNodeMaps {
program: ts.Program;
}
export interface ParserServicesWithoutTypeInformation
extends ParserServicesNodeMaps {
program: null;
}
export type ParserServices =
| ParserServicesWithTypeInformation
| ParserServicesWithoutTypeInformation;

export interface ModuleResolver {
version: 1;
Expand Down

0 comments on commit 7fc062a

Please sign in to comment.