Skip to content

Commit

Permalink
feat(typescript-estree): add EXPERIMENTAL_useProjectService option to…
Browse files Browse the repository at this point in the history
… use TypeScript project service (#6754)

* WIP: createProjectService

* Collected updates: reuse program; lean more into tsserver

* chore: fix website config.ts type checking

* Spell checking

* remove .only

* Cleaned up todo comments in code and properly restrict new option

* Added separate test run in CI for experimental option

* fix: no node-version

* Remove process.env manual set

* Fix linter config.ts

* Tweaked project creation to try to explicitly set cwd

* Progress on updating unit tests for absolute paths

* fix: add missing clearTSServerProjectService

* Add more path relativity fixes

* Lint fixes and watch program relativity

* No, not always true

* Fix around semanticInfo.test.ts

* Switch snapshot to inline

* perf: only openExternalProject once per project

* Revert "perf: only openExternalProject once per project"

This reverts commit a9aec01.

* Reverted changes to allow alternate TSConfig names

* Remove project existence checking

* Add linting from root

* Refactor CI naming a bit, and bumping to MacOS image...

* Alas, linting from root style runs out of memory

* Added a test, why not

* fix: don't fall back to default program/project creation

* Fixed up more test exclusions

* Move tsserver import to a require

* rename: tsserverType -> ts

* Use path.resolve, and then simplify parserSettings usage

* Remove unneeded typingsInstaller
  • Loading branch information
JoshuaKGoldberg committed Jul 16, 2023
1 parent 606a52c commit 6d3d162
Show file tree
Hide file tree
Showing 37 changed files with 809 additions and 556 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Expand Up @@ -193,6 +193,38 @@ jobs:
# Sadly 1 day is the minimum
retention-days: 1

unit_tests_tsserver:
name: Run Unit Tests with Experimental TSServer
needs: [build]
runs-on: ubuntu-latest
strategy:
matrix:
package:
[
'eslint-plugin',
'eslint-plugin-internal',
'eslint-plugin-tslint',
'typescript-estree',
]
env:
COLLECT_COVERAGE: false
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Install
uses: ./.github/actions/prepare-install
with:
node-version: 18
- name: Build
uses: ./.github/actions/prepare-build
- name: Run unit tests for ${{ matrix.package }}
run: npx nx test ${{ matrix.package }} --coverage=false
env:
CI: true
TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER: true

website_tests:
# The NETLIFY_TOKEN secret will not be available on forks
if: github.repository_owner == 'typescript-eslint'
Expand Down
34 changes: 34 additions & 0 deletions .vscode/launch.json
Expand Up @@ -141,6 +141,40 @@
"${workspaceFolder}/packages/scope-manager/dist/index.js",
],
},
{
"type": "node",
"request": "launch",
"name": "Jest Test Current eslint-plugin-tslint Rule",
"cwd": "${workspaceFolder}/packages/eslint-plugin-tslint/",
"program": "${workspaceFolder}/node_modules/jest/bin/jest.js",
"args": [
"--runInBand",
"--no-cache",
"--no-coverage",
"${fileBasenameNoExtension}"
],
"sourceMaps": true,
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"skipFiles": [
"${workspaceFolder}/packages/utils/src/index.ts",
"${workspaceFolder}/packages/utils/dist/index.js",
"${workspaceFolder}/packages/utils/src/ts-estree.ts",
"${workspaceFolder}/packages/utils/dist/ts-estree.js",
"${workspaceFolder}/packages/type-utils/src/index.ts",
"${workspaceFolder}/packages/type-utils/dist/index.js",
"${workspaceFolder}/packages/parser/src/index.ts",
"${workspaceFolder}/packages/parser/dist/index.js",
"${workspaceFolder}/packages/typescript-estree/src/index.ts",
"${workspaceFolder}/packages/typescript-estree/dist/index.js",
"${workspaceFolder}/packages/types/src/index.ts",
"${workspaceFolder}/packages/types/dist/index.js",
"${workspaceFolder}/packages/visitor-keys/src/index.ts",
"${workspaceFolder}/packages/visitor-keys/dist/index.js",
"${workspaceFolder}/packages/scope-manager/dist/index.js",
"${workspaceFolder}/packages/scope-manager/dist/index.js",
],
},
{
"type": "node",
"request": "launch",
Expand Down
11 changes: 10 additions & 1 deletion docs/packages/TypeScript_ESTree.mdx
Expand Up @@ -147,6 +147,15 @@ interface ParseAndGenerateServicesOptions extends ParseOptions {
*/
errorOnTypeScriptSyntacticAndSemanticIssues?: boolean;

/**
* ***EXPERIMENTAL FLAG*** - Use this at your own risk.
*
* Whether to create a shared TypeScript server to power program creation.
*
* @see https://github.com/typescript-eslint/typescript-eslint/issues/6575
*/
EXPERIMENTAL_useProjectService?: boolean;

/**
* ***EXPERIMENTAL FLAG*** - Use this at your own risk.
*
Expand All @@ -155,7 +164,7 @@ interface ParseAndGenerateServicesOptions extends ParseOptions {
*
* This flag REQUIRES at least TS v3.9, otherwise it does nothing.
*
* See: https://github.com/typescript-eslint/typescript-eslint/issues/2094
* @see https://github.com/typescript-eslint/typescript-eslint/issues/2094
*/
EXPERIMENTAL_useSourceOfProjectReferenceRedirect?: boolean;

Expand Down
3 changes: 2 additions & 1 deletion packages/eslint-plugin-tslint/src/rules/config.ts
@@ -1,4 +1,5 @@
import { ESLintUtils } from '@typescript-eslint/utils';
import path from 'path';
import type { RuleSeverity } from 'tslint';
import { Configuration } from 'tslint';

Expand Down Expand Up @@ -118,7 +119,7 @@ export default createRule<Options, MessageIds>({
context,
[{ rules: tslintRules, rulesDirectory: tslintRulesDirectory, lintFile }],
) {
const fileName = context.getFilename();
const fileName = path.resolve(context.getCwd(), context.getFilename());
const sourceCode = context.getSourceCode().text;
const services = ESLintUtils.getParserServices(context);
const program = services.program;
Expand Down
Expand Up @@ -16,6 +16,7 @@ const ruleTester = new RuleTester({
});

const withMetaParserOptions = {
EXPERIMENTAL_useProjectService: false,
tsconfigRootDir: getFixturesRootDir(),
project: './tsconfig-withmeta.json',
};
Expand Down
Expand Up @@ -595,6 +595,7 @@ function getElem(dict: Record<string, { foo: string }>, key: string) {
}
`,
parserOptions: {
EXPERIMENTAL_useProjectService: false,
tsconfigRootDir: getFixturesRootDir(),
project: './tsconfig.noUncheckedIndexedAccess.json',
},
Expand Down
Expand Up @@ -65,6 +65,7 @@ function assignmentTest(
const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
parserOptions: {
EXPERIMENTAL_useProjectService: false,
project: './tsconfig.noImplicitThis.json',
tsconfigRootDir: getFixturesRootDir(),
},
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/tests/rules/no-unsafe-call.test.ts
Expand Up @@ -6,6 +6,7 @@ import { getFixturesRootDir } from '../RuleTester';
const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
parserOptions: {
EXPERIMENTAL_useProjectService: false,
project: './tsconfig.noImplicitThis.json',
tsconfigRootDir: getFixturesRootDir(),
},
Expand Down
Expand Up @@ -6,6 +6,7 @@ import { getFixturesRootDir } from '../RuleTester';
const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
parserOptions: {
EXPERIMENTAL_useProjectService: false,
project: './tsconfig.noImplicitThis.json',
tsconfigRootDir: getFixturesRootDir(),
},
Expand Down
Expand Up @@ -6,6 +6,7 @@ import { getFixturesRootDir } from '../RuleTester';
const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
parserOptions: {
EXPERIMENTAL_useProjectService: false,
project: './tsconfig.noImplicitThis.json',
tsconfigRootDir: getFixturesRootDir(),
},
Expand Down
Expand Up @@ -14,6 +14,7 @@ const ruleTester = new RuleTester({
});

const withMetaParserOptions = {
EXPERIMENTAL_useProjectService: false,
tsconfigRootDir: getFixturesRootDir(),
project: './tsconfig-withmeta.json',
};
Expand Down
Expand Up @@ -204,6 +204,7 @@ const y = x!;

const ruleTesterWithNoUncheckedIndexAccess = new RuleTester({
parserOptions: {
EXPERIMENTAL_useProjectService: false,
sourceType: 'module',
tsconfigRootDir: getFixturesRootDir(),
project: './tsconfig.noUncheckedIndexedAccess.json',
Expand Down
7 changes: 4 additions & 3 deletions packages/parser/tests/lib/parser.ts
@@ -1,6 +1,7 @@
import * as scopeManager from '@typescript-eslint/scope-manager';
import type { ParserOptions } from '@typescript-eslint/types';
import * as typescriptESTree from '@typescript-eslint/typescript-estree';
import path from 'path';

import { parse, parseForESLint } from '../../src/parser';

Expand Down Expand Up @@ -33,10 +34,10 @@ describe('parser', () => {
jsx: false,
},
// ts-estree specific
filePath: 'isolated-file.src.ts',
filePath: './isolated-file.src.ts',
project: 'tsconfig.json',
errorOnTypeScriptSyntacticAndSemanticIssues: false,
tsconfigRootDir: 'tests/fixtures/services',
tsconfigRootDir: path.resolve(__dirname, '../fixtures/services'),
extraFileExtensions: ['.foo'],
};
parseForESLint(code, config);
Expand Down Expand Up @@ -89,7 +90,7 @@ describe('parser', () => {
filePath: 'isolated-file.src.ts',
project: 'tsconfig.json',
errorOnTypeScriptSyntacticAndSemanticIssues: false,
tsconfigRootDir: 'tests/fixtures/services',
tsconfigRootDir: path.join(__dirname, '../fixtures/services'),
extraFileExtensions: ['.foo'],
};
parseForESLint(code, config);
Expand Down
2 changes: 2 additions & 0 deletions packages/rule-tester/tests/RuleTester.test.ts
Expand Up @@ -169,6 +169,7 @@ describe('RuleTester', () => {
{
code: 'type-aware parser options should override the constructor config',
parserOptions: {
EXPERIMENTAL_useProjectService: false,
project: 'tsconfig.test-specific.json',
tsconfigRootDir: '/set/in/the/test/',
},
Expand Down Expand Up @@ -209,6 +210,7 @@ describe('RuleTester', () => {
"code": "type-aware parser options should override the constructor config",
"filename": "/set/in/the/test/file.ts",
"parserOptions": {
"EXPERIMENTAL_useProjectService": false,
"project": "tsconfig.test-specific.json",
"tsconfigRootDir": "/set/in/the/test/",
},
Expand Down
20 changes: 10 additions & 10 deletions packages/type-utils/tests/TypeOrValueSpecifier.test.ts
Expand Up @@ -196,42 +196,42 @@ describe('TypeOrValueSpecifier', () => {
],
[
'interface Foo {prop: string}; type Test = Foo;',
{ from: 'file', name: 'Foo', path: 'tests/fixtures/file.ts' },
{ from: 'file', name: 'Foo', path: 'file.ts' },
],
[
'type Foo = {prop: string}; type Test = Foo;',
{ from: 'file', name: 'Foo', path: 'tests/fixtures/file.ts' },
{ from: 'file', name: 'Foo', path: 'file.ts' },
],
[
'interface Foo {prop: string}; type Test = Foo;',
{
from: 'file',
name: 'Foo',
path: 'tests/../tests/fixtures/////file.ts',
path: './////file.ts',
},
],
[
'type Foo = {prop: string}; type Test = Foo;',
{
from: 'file',
name: 'Foo',
path: 'tests/../tests/fixtures/////file.ts',
path: './////file.ts',
},
],
[
'interface Foo {prop: string}; type Test = Foo;',
{
from: 'file',
name: ['Foo', 'Bar'],
path: 'tests/fixtures/file.ts',
path: 'file.ts',
},
],
[
'type Foo = {prop: string}; type Test = Foo;',
{
from: 'file',
name: ['Foo', 'Bar'],
path: 'tests/fixtures/file.ts',
path: 'file.ts',
},
],
])('matches a matching file specifier: %s', runTestPositive);
Expand All @@ -247,14 +247,14 @@ describe('TypeOrValueSpecifier', () => {
],
[
'interface Foo {prop: string}; type Test = Foo;',
{ from: 'file', name: 'Foo', path: 'tests/fixtures/wrong-file.ts' },
{ from: 'file', name: 'Foo', path: 'wrong-file.ts' },
],
[
'interface Foo {prop: string}; type Test = Foo;',
{
from: 'file',
name: ['Foo', 'Bar'],
path: 'tests/fixtures/wrong-file.ts',
path: 'wrong-file.ts',
},
],
])("doesn't match a mismatched file specifier: %s", runTestNegative);
Expand Down Expand Up @@ -399,14 +399,14 @@ describe('TypeOrValueSpecifier', () => {
['type Test = RegExp;', { from: 'file', name: ['RegExp', 'BigInt'] }],
[
'type Test = RegExp;',
{ from: 'file', name: 'RegExp', path: 'tests/fixtures/file.ts' },
{ from: 'file', name: 'RegExp', path: 'file.ts' },
],
[
'type Test = RegExp;',
{
from: 'file',
name: ['RegExp', 'BigInt'],
path: 'tests/fixtures/file.ts',
path: 'file.ts',
},
],
[
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/parser-options.ts
Expand Up @@ -47,6 +47,7 @@ interface ParserOptions {
debugLevel?: DebugLevel;
errorOnTypeScriptSyntacticAndSemanticIssues?: boolean;
errorOnUnknownASTType?: boolean;
EXPERIMENTAL_useProjectService?: boolean; // purposely undocumented for now
EXPERIMENTAL_useSourceOfProjectReferenceRedirect?: boolean; // purposely undocumented for now
extraFileExtensions?: string[];
filePath?: string;
Expand Down
6 changes: 5 additions & 1 deletion packages/typescript-estree/src/clear-caches.ts
@@ -1,6 +1,9 @@
import { clearWatchCaches } from './create-program/getWatchProgramsForProjects';
import { clearProgramCache as clearProgramCacheOriginal } from './parser';
import { clearTSConfigMatchCache } from './parseSettings/createParseSettings';
import {
clearTSConfigMatchCache,
clearTSServerProjectService,
} from './parseSettings/createParseSettings';
import { clearGlobCache } from './parseSettings/resolveProjectList';

/**
Expand All @@ -14,6 +17,7 @@ export function clearCaches(): void {
clearProgramCacheOriginal();
clearWatchCaches();
clearTSConfigMatchCache();
clearTSServerProjectService();
clearGlobCache();
}

Expand Down
Expand Up @@ -5,7 +5,6 @@ import * as ts from 'typescript';
import { firstDefined } from '../node-utils';
import type { ParseSettings } from '../parseSettings';
import { describeFilePath } from './describeFilePath';
import { getWatchProgramsForProjects } from './getWatchProgramsForProjects';
import type { ASTAndDefiniteProgram } from './shared';
import { getAstFromProgram } from './shared';

Expand All @@ -28,12 +27,12 @@ const DEFAULT_EXTRA_FILE_EXTENSIONS = [
*/
function createProjectProgram(
parseSettings: ParseSettings,
programsForProjects: readonly ts.Program[],
): ASTAndDefiniteProgram | undefined {
log('Creating project program for: %s', parseSettings.filePath);

const programsForProjects = getWatchProgramsForProjects(parseSettings);
const astAndProgram = firstDefined(programsForProjects, currentProgram =>
getAstFromProgram(currentProgram, parseSettings),
getAstFromProgram(currentProgram, parseSettings.filePath),
);

// The file was either matched within the tsconfig, or we allow creating a default program
Expand Down

0 comments on commit 6d3d162

Please sign in to comment.