Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
BioPhoton committed Dec 27, 2024
1 parent 58bb4f8 commit d7d797c
Show file tree
Hide file tree
Showing 18 changed files with 247 additions and 298 deletions.
2 changes: 1 addition & 1 deletion packages/plugin-typescript/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import baseConfig from '../../eslint.config.js';
export default tseslint.config(
...baseConfig,
{
files: ['**/*.ts', '!**/default-ts-configs'],
files: ['**/*.ts', '!**/default-ts-configs', '!**/mocks'],
languageOptions: {
parserOptions: {
projectService: true,
Expand Down
31 changes: 23 additions & 8 deletions packages/plugin-typescript/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import type { Audit, Group } from '@code-pushup/models';
import { camelCaseToKebabCase, kebabCaseToSentence } from '@code-pushup/utils';
import {
GROUPS_DESCRIPTIONS,
TS_ERROR_CODES,
} from './runner/ts-error-codes.js';
import type { CompilerOptionName } from './types.js';
import { TS_ERROR_CODES } from './runner/ts-error-codes.js';
import type { CompilerOptionName } from './runner/types.js';

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

const weights = {
const GROUP_WEIGHTS: Partial<Record<keyof typeof TS_ERROR_CODES, number>> = {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
strictChecks: 3,
strict: 3,
typeCheckingBehavior: 2,
controlFlowOptions: 2,
interopConstraints: 2,
};

const GROUPS_DESCRIPTIONS: Record<keyof typeof TS_ERROR_CODES, string> = {
languageAndEnvironment:
'Configuration options for TypeScript language features and runtime environment, including decorators, JSX support, target ECMAScript version, and class field behaviors',
interopConstraints:
'Settings that control how TypeScript interoperates with other JavaScript code, including module imports/exports and case sensitivity rules',
moduleResolution:
'Settings that control how TypeScript finds and resolves module imports, including Node.js resolution, package.json exports/imports, and module syntax handling',
typeCheckingBehavior:
'Configuration for TypeScript type checking strictness and error reporting, including property access rules and method override checking',
controlFlowOptions:
'Settings that affect code flow analysis, including handling of unreachable code, unused labels, switch statements, and async/generator functions',
strict:
'Strict type checking options that enable additional compile-time verifications, including null checks, implicit any/this, and function type checking',
buildEmitOptions:
'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',
};

export const GROUPS: Group[] = Object.entries(TS_ERROR_CODES).map(
([groupSlug, auditMap]) => ({
slug: camelCaseToKebabCase(groupSlug),
Expand All @@ -39,7 +54,7 @@ export const GROUPS: Group[] = Object.entries(TS_ERROR_CODES).map(
GROUPS_DESCRIPTIONS[groupSlug as keyof typeof GROUPS_DESCRIPTIONS],
refs: Object.keys(auditMap).map(audit => ({
slug: camelCaseToKebabCase(audit),
weight: weights[audit as keyof typeof weights] ?? 1,
weight: GROUP_WEIGHTS[audit as keyof typeof GROUP_WEIGHTS] ?? 1,
})),
}),
);
13 changes: 13 additions & 0 deletions packages/plugin-typescript/src/lib/runner/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { camelCaseToKebabCase } from '@code-pushup/utils';
import { TS_ERROR_CODES } from './ts-error-codes.js';
import type { CompilerOptionName } from './types.js';

/** Build Reverse Lookup Map. It will a map with key as the error code and value as the audit slug. */
export const AUDIT_LOOKUP = Object.values(TS_ERROR_CODES)
.flatMap(v => Object.entries(v))
.reduce<Map<number, CompilerOptionName>>((lookup, [name, codes]) => {
codes.forEach((code: number) =>
lookup.set(code, camelCaseToKebabCase(name) as CompilerOptionName),
);
return lookup;
}, new Map<number, CompilerOptionName>());
22 changes: 9 additions & 13 deletions packages/plugin-typescript/src/lib/runner/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,19 @@ import type {
Issue,
RunnerFunction,
} from '@code-pushup/models';
import type { CompilerOptionName, TypescriptPluginOptions } from '../types.js';
import { getDiagnostics } from './typescript-runner.js';
import {
AUDIT_LOOKUP,
getIssueFromDiagnostic,
tSCodeToAuditSlug,
validateDiagnostics,
} from './utils.js';
import type { TypescriptPluginOptions } from '../types.js';
import { AUDIT_LOOKUP } from './constants.js';
import { getTypeScriptDiagnostics } from './ts-runner.js';
import type { CompilerOptionName } from './types.js';
import { getIssueFromDiagnostic, tSCodeToAuditSlug } from './utils.js';

export type RunnerOptions = TypescriptPluginOptions & {
filteredAudits: Audit[];
expectedAudits: Audit[];
};

export function createRunnerFunction(options: RunnerOptions): RunnerFunction {
return async (): Promise<AuditOutputs> => {
const diagnostics = await getDiagnostics(options.tsConfigPath);
validateDiagnostics(diagnostics);
const diagnostics = await getTypeScriptDiagnostics(options.tsConfigPath);
const result: Record<
CompilerOptionName,
Pick<AuditReport, 'slug' | 'details'>
Expand Down Expand Up @@ -50,8 +46,8 @@ export function createRunnerFunction(options: RunnerOptions): RunnerFunction {
>,
);

return options.filteredAudits.map(audit => {
const { details } = result[audit.slug as CompilerOptionName] ?? {};
return options.expectedAudits.map(audit => {
const { details } = result[audit.slug as CompilerOptionName];
const issues = details?.issues ?? [];
return {
...audit,
Expand Down
21 changes: 0 additions & 21 deletions packages/plugin-typescript/src/lib/runner/ts-error-codes.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,5 @@
/* eslint-disable @typescript-eslint/no-magic-numbers, unicorn/numeric-separators-style */

export const GROUPS_DESCRIPTIONS = {
languageAndEnvironment:
'Configuration options for TypeScript language features and runtime environment, including decorators, JSX support, target ECMAScript version, and class field behaviors',
interopConstraints:
'Settings that control how TypeScript interoperates with other JavaScript code, including module imports/exports and case sensitivity rules',
watchOptions:
'Configuration for TypeScript watch mode behavior, including file watching strategies and dependency tracking',
projectReferences:
'Options for managing TypeScript project references, composite projects, and build optimization settings',
moduleResolution:
'Settings that control how TypeScript finds and resolves module imports, including Node.js resolution, package.json exports/imports, and module syntax handling',
typeCheckingBehavior:
'Configuration for TypeScript type checking strictness and error reporting, including property access rules and method override checking',
controlFlowOptions:
'Settings that affect code flow analysis, including handling of unreachable code, unused labels, switch statements, and async/generator functions',
strictChecks:
'Strict type checking options that enable additional compile-time verifications, including null checks, implicit any/this, and function type checking',
buildEmitOptions:
'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',
};

/**
* Strict grouping: https://github.com/microsoft/TypeScript/blob/56a08250f3516b3f5bc120d6c7ab4450a9a69352/src/compiler/utilities.ts Line 9113
* noImplicitThis: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { describe, expect } from 'vitest';
import { getTsConfigurationFromPath } from './typescript-runner.js';
import { getTypeScriptDiagnostics } from './ts-runner.js';

describe('getTsConfigurationFromPath', () => {
describe('getTypeScriptDiagnostics', () => {
it('should accept valid options', async () => {
await expect(
getTsConfigurationFromPath({
tsConfigPath:
'packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.json',
}),
getTypeScriptDiagnostics(
'packages/plugin-typescript/mocks/fixtures/basic-setup/tsconfig.json',
),
).resolves.toStrictEqual({
compilerOptions: {
configFilePath: undefined,
Expand Down
41 changes: 41 additions & 0 deletions packages/plugin-typescript/src/lib/runner/ts-runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
type CompilerOptions,
type Diagnostic,
createProgram,
getPreEmitDiagnostics,
} from 'typescript';
import { AUDIT_LOOKUP } from './constants.js';
import { loadTargetConfig } from './utils.js';

export type DiagnosticsOptions = {
fileNames: string[];
compilerOptions: CompilerOptions;
};

export async function getTypeScriptDiagnostics(
tsConfigPath: string,
): Promise<readonly Diagnostic[]> {
try {
const { fileNames, options } = await loadTargetConfig(tsConfigPath);
const program = createProgram(fileNames, options);

const diagnostics = getPreEmitDiagnostics(program);
validateDiagnostics(diagnostics);

return diagnostics;
} catch (error) {
throw new Error(
`Can't create TS program in getDiagnostics. \n ${(error as Error).message}`,
);
}
}

export function validateDiagnostics(diagnostics: readonly Diagnostic[]) {
diagnostics
.filter(({ code }) => !AUDIT_LOOKUP.has(code))
.forEach(({ code, messageText }) => {
console.warn(
`Diagnostic Warning: The code ${code} is not supported. ${messageText}`,
);
});
}
9 changes: 9 additions & 0 deletions packages/plugin-typescript/src/lib/runner/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { TS_ERROR_CODES } from './ts-error-codes.js';

export type ErrorCodes = typeof TS_ERROR_CODES;

export type CompilerOptionName = {
[K in keyof ErrorCodes]: keyof ErrorCodes[K];
}[keyof ErrorCodes];

export type SemVerString = `${number}.${number}.${number}`;
64 changes: 0 additions & 64 deletions packages/plugin-typescript/src/lib/runner/typescript-runner.ts

This file was deleted.

95 changes: 70 additions & 25 deletions packages/plugin-typescript/src/lib/runner/utils.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import { access } from 'node:fs/promises';
// eslint-disable-next-line unicorn/import-style
import { dirname } from 'node:path';
import {
type CompilerOptions,
type Diagnostic,
DiagnosticCategory,
type ParsedCommandLine,
flattenDiagnosticMessageText,
parseConfigFileTextToJson,
parseJsonConfigFileContent,
sys,
} from 'typescript';
import type { Issue } from '@code-pushup/models';
import { camelCaseToKebabCase, truncateIssueMessage } from '@code-pushup/utils';
import type { CompilerOptionName } from '../types.js';
import { TS_ERROR_CODES } from './ts-error-codes.js';

/** Build Reverse Lookup Map. It will a map with key as the error code and value as the audit slug. */
export const AUDIT_LOOKUP = Object.values(TS_ERROR_CODES)
.flatMap(v => Object.entries(v))
.reduce<Map<number, CompilerOptionName>>((lookup, [name, codes]) => {
codes.forEach((code: number) =>
lookup.set(code, camelCaseToKebabCase(name) as CompilerOptionName),
);
return lookup;
}, new Map<number, CompilerOptionName>());
import {
executeProcess,
readTextFile,
truncateIssueMessage,
} from '@code-pushup/utils';
import { AUDIT_LOOKUP } from './constants.js';
import type { CompilerOptionName, SemVerString } from './types.js';

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

//OK DOOONE, now it's more beautiful, goodbye! let me know when u finish if u want
// I was getting so frustrated of with webstorm sry xD
export function validateDiagnostics(diagnostics: readonly Diagnostic[]) {
diagnostics
.filter(({ code }) => !AUDIT_LOOKUP.has(code))
.forEach(({ code, messageText }) => {
console.warn(
`Diagnostic Warning: The code ${code} is not supported. ${messageText}`,
);
});
}

/**
* Get the severity of the issue based on the TypeScript diagnostic category.
* - ts.DiagnosticCategory.Warning (1)
Expand Down Expand Up @@ -98,3 +88,58 @@ export function getIssueFromDiagnostic(diag: Diagnostic) {
},
} satisfies Issue;
}

const _TS_CONFIG_MAP = new Map<string, ParsedCommandLine>();
export async function loadTargetConfig(tsConfigPath: string) {
if (_TS_CONFIG_MAP.get(tsConfigPath) === undefined) {
const { config } = parseConfigFileTextToJson(
tsConfigPath,
await readTextFile(tsConfigPath),
);

const parsedConfig = parseJsonConfigFileContent(
config,
sys,
dirname(tsConfigPath),
);

if (parsedConfig.fileNames.length === 0) {
throw new Error(
'No files matched by the TypeScript configuration. Check your "include", "exclude" or "files" settings.',
);
}

_TS_CONFIG_MAP.set(tsConfigPath, parsedConfig);
}
return _TS_CONFIG_MAP.get(tsConfigPath) as ParsedCommandLine;
}

export async function getCurrentTsVersion(): Promise<SemVerString> {
const { stdout } = await executeProcess({
command: 'npx',
args: ['-y', 'tsc', '--version'],
});
return stdout.split(' ').slice(-1).join('').trim() as SemVerString;
}

export async function loadTsConfigDefaultsByVersion(version: SemVerString) {
const __dirname = new URL('.', import.meta.url).pathname;
const configPath = `${__dirname}default-ts-configs/${version}.ts`;

try {
await access(configPath);
} catch {
throw new Error(
`Could not find default TS config for version ${version}. R The plugin maintainer has to support this version.`,
);
}

try {
const module = await import(configPath);
return module.default as { compilerOptions: CompilerOptions };
} catch (error) {
throw new Error(
`Could load default TS config for version ${version}. /n ${(error as Error).message}`,
);
}
}
Loading

0 comments on commit d7d797c

Please sign in to comment.