Skip to content

Commit d7d797c

Browse files
committed
wip
1 parent 58bb4f8 commit d7d797c

18 files changed

+247
-298
lines changed

packages/plugin-typescript/eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import baseConfig from '../../eslint.config.js';
44
export default tseslint.config(
55
...baseConfig,
66
{
7-
files: ['**/*.ts', '!**/default-ts-configs'],
7+
files: ['**/*.ts', '!**/default-ts-configs', '!**/mocks'],
88
languageOptions: {
99
parserOptions: {
1010
projectService: true,

packages/plugin-typescript/src/lib/constants.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import type { Audit, Group } from '@code-pushup/models';
22
import { camelCaseToKebabCase, kebabCaseToSentence } from '@code-pushup/utils';
3-
import {
4-
GROUPS_DESCRIPTIONS,
5-
TS_ERROR_CODES,
6-
} from './runner/ts-error-codes.js';
7-
import type { CompilerOptionName } from './types.js';
3+
import { TS_ERROR_CODES } from './runner/ts-error-codes.js';
4+
import type { CompilerOptionName } from './runner/types.js';
85

96
export const TYPESCRIPT_PLUGIN_SLUG = 'typescript';
107
export const DEFAULT_TS_CONFIG = 'tsconfig.json';
@@ -24,13 +21,31 @@ export const AUDITS = Object.values(TS_ERROR_CODES)
2421
];
2522
}, []);
2623

27-
const weights = {
24+
const GROUP_WEIGHTS: Partial<Record<keyof typeof TS_ERROR_CODES, number>> = {
2825
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
29-
strictChecks: 3,
26+
strict: 3,
3027
typeCheckingBehavior: 2,
3128
controlFlowOptions: 2,
3229
interopConstraints: 2,
3330
};
31+
32+
const GROUPS_DESCRIPTIONS: Record<keyof typeof TS_ERROR_CODES, string> = {
33+
languageAndEnvironment:
34+
'Configuration options for TypeScript language features and runtime environment, including decorators, JSX support, target ECMAScript version, and class field behaviors',
35+
interopConstraints:
36+
'Settings that control how TypeScript interoperates with other JavaScript code, including module imports/exports and case sensitivity rules',
37+
moduleResolution:
38+
'Settings that control how TypeScript finds and resolves module imports, including Node.js resolution, package.json exports/imports, and module syntax handling',
39+
typeCheckingBehavior:
40+
'Configuration for TypeScript type checking strictness and error reporting, including property access rules and method override checking',
41+
controlFlowOptions:
42+
'Settings that affect code flow analysis, including handling of unreachable code, unused labels, switch statements, and async/generator functions',
43+
strict:
44+
'Strict type checking options that enable additional compile-time verifications, including null checks, implicit any/this, and function type checking',
45+
buildEmitOptions:
46+
'Configuration options that control TypeScript output generation, including whether to emit files, how to handle comments and declarations, and settings for output optimization and compatibility helpers',
47+
};
48+
3449
export const GROUPS: Group[] = Object.entries(TS_ERROR_CODES).map(
3550
([groupSlug, auditMap]) => ({
3651
slug: camelCaseToKebabCase(groupSlug),
@@ -39,7 +54,7 @@ export const GROUPS: Group[] = Object.entries(TS_ERROR_CODES).map(
3954
GROUPS_DESCRIPTIONS[groupSlug as keyof typeof GROUPS_DESCRIPTIONS],
4055
refs: Object.keys(auditMap).map(audit => ({
4156
slug: camelCaseToKebabCase(audit),
42-
weight: weights[audit as keyof typeof weights] ?? 1,
57+
weight: GROUP_WEIGHTS[audit as keyof typeof GROUP_WEIGHTS] ?? 1,
4358
})),
4459
}),
4560
);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { camelCaseToKebabCase } from '@code-pushup/utils';
2+
import { TS_ERROR_CODES } from './ts-error-codes.js';
3+
import type { CompilerOptionName } from './types.js';
4+
5+
/** Build Reverse Lookup Map. It will a map with key as the error code and value as the audit slug. */
6+
export const AUDIT_LOOKUP = Object.values(TS_ERROR_CODES)
7+
.flatMap(v => Object.entries(v))
8+
.reduce<Map<number, CompilerOptionName>>((lookup, [name, codes]) => {
9+
codes.forEach((code: number) =>
10+
lookup.set(code, camelCaseToKebabCase(name) as CompilerOptionName),
11+
);
12+
return lookup;
13+
}, new Map<number, CompilerOptionName>());

packages/plugin-typescript/src/lib/runner/runner.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,19 @@ import type {
66
Issue,
77
RunnerFunction,
88
} from '@code-pushup/models';
9-
import type { CompilerOptionName, TypescriptPluginOptions } from '../types.js';
10-
import { getDiagnostics } from './typescript-runner.js';
11-
import {
12-
AUDIT_LOOKUP,
13-
getIssueFromDiagnostic,
14-
tSCodeToAuditSlug,
15-
validateDiagnostics,
16-
} from './utils.js';
9+
import type { TypescriptPluginOptions } from '../types.js';
10+
import { AUDIT_LOOKUP } from './constants.js';
11+
import { getTypeScriptDiagnostics } from './ts-runner.js';
12+
import type { CompilerOptionName } from './types.js';
13+
import { getIssueFromDiagnostic, tSCodeToAuditSlug } from './utils.js';
1714

1815
export type RunnerOptions = TypescriptPluginOptions & {
19-
filteredAudits: Audit[];
16+
expectedAudits: Audit[];
2017
};
2118

2219
export function createRunnerFunction(options: RunnerOptions): RunnerFunction {
2320
return async (): Promise<AuditOutputs> => {
24-
const diagnostics = await getDiagnostics(options.tsConfigPath);
25-
validateDiagnostics(diagnostics);
21+
const diagnostics = await getTypeScriptDiagnostics(options.tsConfigPath);
2622
const result: Record<
2723
CompilerOptionName,
2824
Pick<AuditReport, 'slug' | 'details'>
@@ -50,8 +46,8 @@ export function createRunnerFunction(options: RunnerOptions): RunnerFunction {
5046
>,
5147
);
5248

53-
return options.filteredAudits.map(audit => {
54-
const { details } = result[audit.slug as CompilerOptionName] ?? {};
49+
return options.expectedAudits.map(audit => {
50+
const { details } = result[audit.slug as CompilerOptionName];
5551
const issues = details?.issues ?? [];
5652
return {
5753
...audit,

packages/plugin-typescript/src/lib/runner/ts-error-codes.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-magic-numbers, unicorn/numeric-separators-style */
22

3-
export const GROUPS_DESCRIPTIONS = {
4-
languageAndEnvironment:
5-
'Configuration options for TypeScript language features and runtime environment, including decorators, JSX support, target ECMAScript version, and class field behaviors',
6-
interopConstraints:
7-
'Settings that control how TypeScript interoperates with other JavaScript code, including module imports/exports and case sensitivity rules',
8-
watchOptions:
9-
'Configuration for TypeScript watch mode behavior, including file watching strategies and dependency tracking',
10-
projectReferences:
11-
'Options for managing TypeScript project references, composite projects, and build optimization settings',
12-
moduleResolution:
13-
'Settings that control how TypeScript finds and resolves module imports, including Node.js resolution, package.json exports/imports, and module syntax handling',
14-
typeCheckingBehavior:
15-
'Configuration for TypeScript type checking strictness and error reporting, including property access rules and method override checking',
16-
controlFlowOptions:
17-
'Settings that affect code flow analysis, including handling of unreachable code, unused labels, switch statements, and async/generator functions',
18-
strictChecks:
19-
'Strict type checking options that enable additional compile-time verifications, including null checks, implicit any/this, and function type checking',
20-
buildEmitOptions:
21-
'Configuration options that control TypeScript output generation, including whether to emit files, how to handle comments and declarations, and settings for output optimization and compatibility helpers',
22-
};
23-
243
/**
254
* Strict grouping: https://github.com/microsoft/TypeScript/blob/56a08250f3516b3f5bc120d6c7ab4450a9a69352/src/compiler/utilities.ts Line 9113
265
* noImplicitThis: {

packages/plugin-typescript/src/lib/runner/typescript-runner.integration.test.ts renamed to packages/plugin-typescript/src/lib/runner/ts-runner.integration.test.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { describe, expect } from 'vitest';
2-
import { getTsConfigurationFromPath } from './typescript-runner.js';
2+
import { getTypeScriptDiagnostics } from './ts-runner.js';
33

4-
describe('getTsConfigurationFromPath', () => {
4+
describe('getTypeScriptDiagnostics', () => {
55
it('should accept valid options', async () => {
66
await expect(
7-
getTsConfigurationFromPath({
8-
tsConfigPath:
9-
'packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.json',
10-
}),
7+
getTypeScriptDiagnostics(
8+
'packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.json',
9+
),
1110
).resolves.toStrictEqual({
1211
compilerOptions: {
1312
configFilePath: undefined,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {
2+
type CompilerOptions,
3+
type Diagnostic,
4+
createProgram,
5+
getPreEmitDiagnostics,
6+
} from 'typescript';
7+
import { AUDIT_LOOKUP } from './constants.js';
8+
import { loadTargetConfig } from './utils.js';
9+
10+
export type DiagnosticsOptions = {
11+
fileNames: string[];
12+
compilerOptions: CompilerOptions;
13+
};
14+
15+
export async function getTypeScriptDiagnostics(
16+
tsConfigPath: string,
17+
): Promise<readonly Diagnostic[]> {
18+
try {
19+
const { fileNames, options } = await loadTargetConfig(tsConfigPath);
20+
const program = createProgram(fileNames, options);
21+
22+
const diagnostics = getPreEmitDiagnostics(program);
23+
validateDiagnostics(diagnostics);
24+
25+
return diagnostics;
26+
} catch (error) {
27+
throw new Error(
28+
`Can't create TS program in getDiagnostics. \n ${(error as Error).message}`,
29+
);
30+
}
31+
}
32+
33+
export function validateDiagnostics(diagnostics: readonly Diagnostic[]) {
34+
diagnostics
35+
.filter(({ code }) => !AUDIT_LOOKUP.has(code))
36+
.forEach(({ code, messageText }) => {
37+
console.warn(
38+
`Diagnostic Warning: The code ${code} is not supported. ${messageText}`,
39+
);
40+
});
41+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { TS_ERROR_CODES } from './ts-error-codes.js';
2+
3+
export type ErrorCodes = typeof TS_ERROR_CODES;
4+
5+
export type CompilerOptionName = {
6+
[K in keyof ErrorCodes]: keyof ErrorCodes[K];
7+
}[keyof ErrorCodes];
8+
9+
export type SemVerString = `${number}.${number}.${number}`;

packages/plugin-typescript/src/lib/runner/typescript-runner.ts

Lines changed: 0 additions & 64 deletions
This file was deleted.

packages/plugin-typescript/src/lib/runner/utils.ts

Lines changed: 70 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
1+
import { access } from 'node:fs/promises';
2+
// eslint-disable-next-line unicorn/import-style
3+
import { dirname } from 'node:path';
14
import {
5+
type CompilerOptions,
26
type Diagnostic,
37
DiagnosticCategory,
8+
type ParsedCommandLine,
49
flattenDiagnosticMessageText,
10+
parseConfigFileTextToJson,
11+
parseJsonConfigFileContent,
12+
sys,
513
} from 'typescript';
614
import type { Issue } from '@code-pushup/models';
7-
import { camelCaseToKebabCase, truncateIssueMessage } from '@code-pushup/utils';
8-
import type { CompilerOptionName } from '../types.js';
9-
import { TS_ERROR_CODES } from './ts-error-codes.js';
10-
11-
/** Build Reverse Lookup Map. It will a map with key as the error code and value as the audit slug. */
12-
export const AUDIT_LOOKUP = Object.values(TS_ERROR_CODES)
13-
.flatMap(v => Object.entries(v))
14-
.reduce<Map<number, CompilerOptionName>>((lookup, [name, codes]) => {
15-
codes.forEach((code: number) =>
16-
lookup.set(code, camelCaseToKebabCase(name) as CompilerOptionName),
17-
);
18-
return lookup;
19-
}, new Map<number, CompilerOptionName>());
15+
import {
16+
executeProcess,
17+
readTextFile,
18+
truncateIssueMessage,
19+
} from '@code-pushup/utils';
20+
import { AUDIT_LOOKUP } from './constants.js';
21+
import type { CompilerOptionName, SemVerString } from './types.js';
2022

2123
/**
2224
* Transform the TypeScript error code to the audit slug.
@@ -32,18 +34,6 @@ export function tSCodeToAuditSlug(code: number): CompilerOptionName {
3234
return knownCode;
3335
}
3436

35-
//OK DOOONE, now it's more beautiful, goodbye! let me know when u finish if u want
36-
// I was getting so frustrated of with webstorm sry xD
37-
export function validateDiagnostics(diagnostics: readonly Diagnostic[]) {
38-
diagnostics
39-
.filter(({ code }) => !AUDIT_LOOKUP.has(code))
40-
.forEach(({ code, messageText }) => {
41-
console.warn(
42-
`Diagnostic Warning: The code ${code} is not supported. ${messageText}`,
43-
);
44-
});
45-
}
46-
4737
/**
4838
* Get the severity of the issue based on the TypeScript diagnostic category.
4939
* - ts.DiagnosticCategory.Warning (1)
@@ -98,3 +88,58 @@ export function getIssueFromDiagnostic(diag: Diagnostic) {
9888
},
9989
} satisfies Issue;
10090
}
91+
92+
const _TS_CONFIG_MAP = new Map<string, ParsedCommandLine>();
93+
export async function loadTargetConfig(tsConfigPath: string) {
94+
if (_TS_CONFIG_MAP.get(tsConfigPath) === undefined) {
95+
const { config } = parseConfigFileTextToJson(
96+
tsConfigPath,
97+
await readTextFile(tsConfigPath),
98+
);
99+
100+
const parsedConfig = parseJsonConfigFileContent(
101+
config,
102+
sys,
103+
dirname(tsConfigPath),
104+
);
105+
106+
if (parsedConfig.fileNames.length === 0) {
107+
throw new Error(
108+
'No files matched by the TypeScript configuration. Check your "include", "exclude" or "files" settings.',
109+
);
110+
}
111+
112+
_TS_CONFIG_MAP.set(tsConfigPath, parsedConfig);
113+
}
114+
return _TS_CONFIG_MAP.get(tsConfigPath) as ParsedCommandLine;
115+
}
116+
117+
export async function getCurrentTsVersion(): Promise<SemVerString> {
118+
const { stdout } = await executeProcess({
119+
command: 'npx',
120+
args: ['-y', 'tsc', '--version'],
121+
});
122+
return stdout.split(' ').slice(-1).join('').trim() as SemVerString;
123+
}
124+
125+
export async function loadTsConfigDefaultsByVersion(version: SemVerString) {
126+
const __dirname = new URL('.', import.meta.url).pathname;
127+
const configPath = `${__dirname}default-ts-configs/${version}.ts`;
128+
129+
try {
130+
await access(configPath);
131+
} catch {
132+
throw new Error(
133+
`Could not find default TS config for version ${version}. R The plugin maintainer has to support this version.`,
134+
);
135+
}
136+
137+
try {
138+
const module = await import(configPath);
139+
return module.default as { compilerOptions: CompilerOptions };
140+
} catch (error) {
141+
throw new Error(
142+
`Could load default TS config for version ${version}. /n ${(error as Error).message}`,
143+
);
144+
}
145+
}

0 commit comments

Comments
 (0)