Skip to content
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

feat: remove partial type-information program #6066

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
}
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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,
};
}
bradzacher marked this conversation as resolved.
Show resolved Hide resolved

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;
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
}
export type ParserServices =
| ParserServicesWithTypeInformation
| ParserServicesWithoutTypeInformation;

export interface ModuleResolver {
version: 1;
Expand Down