Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

## Unreleased

- Deprecate `context.rawChecker`. Types overrides from `context.checker` have been updated so it can be passed to other libraries. Thanks @JoshuaKGoldberg for challenging this!
### Add `context.languageService`

This allows to create a first kind of 'multi-file' rules. It's used in the new core rule `core/unusedExport`.

### Deprecate `context.rawChecker`

Types overrides of `context.checker` have been updated so it can be passed to other libraries. `context.rawChecker` is therefore not needed anymore and has been deprecated. Thanks @JoshuaKGoldberg for challenging this!

## 1.0.28

Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,13 @@ export default defineConfig({

## Core rules

Currently, the list of core rules are the type-aware lint rules I use from typescript-eslint. If you think more rules should be added, please open an issue, but to reduce the surface, only non-styling type-aware rules will be accepted. Here is the list of [typescript-eslint type aware rules](https://typescript-eslint.io/rules/?=typeInformation) with their status:
### Exclusive rules

- unusedExport: Detect unused exports

### From typescript-eslint

Currently, the ported rules are the type-aware lint rules I use from typescript-eslint. If you think more rules should be added, please open an issue, but to reduce the surface, only non-styling type-aware rules will be accepted. Here is the list of [typescript-eslint type aware rules](https://typescript-eslint.io/rules/?=typeInformation) with their status:

- await-thenable: ✅ Implemented
- consistent-return: 🛑 Implementation not planned, you can use `noImplicitReturns` compilerOption
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"import": "bun scripts/import-rule.ts",
"test": "bun test",
"build": "bun scripts/bundle.ts && tsc -p tsconfig.dist.json && cd dist && publint",
"tsl": "bun src/cli.ts",
"tsl": "bun src/cli-entrypoint.ts",
"prettier-all": "bun prettier-ci --write",
"prettier-ci": "prettier --cache --check '**/*.{ts,json,md,yml}'",
"check": "bun prettier-ci && bun run test && bun tsl && bun run build",
Expand Down
8 changes: 6 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
formatDiagnostics,
type TSLDiagnostic,
} from "./formatDiagnostic.ts";
import { getLanguageService } from "./getLanguageService.ts";
import { core } from "./index.ts";
import { initRules } from "./initRules.ts";
import { loadConfig } from "./loadConfig.ts";
Expand Down Expand Up @@ -62,12 +63,14 @@ if (result.errors.length) {
}

const host = ts.createCompilerHost(result.options, true);
const program = ts.createProgram({
const programForLanguageServiceHost = ts.createProgram({
rootNames: result.fileNames,
options: result.options,
projectReferences: result.projectReferences,
host,
host: host,
});
const languageService = getLanguageService(programForLanguageServiceHost);
const program = languageService.getProgram()!;

if (values.timing) {
console.log(`TS program created in ${displayTiming(programStart)}`);
Expand All @@ -77,6 +80,7 @@ const configStart = performance.now();
const { config } = await loadConfig(program);
const { lint, allRules, timingMaps } = await initRules(
() => program,
() => languageService,
config ?? { rules: core.all() },
values.timing,
);
Expand Down
2 changes: 1 addition & 1 deletion src/formatDiagnostic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ function formatCodeSpan(
return context;
}

export function formatLocation(file: SourceFile, start: number): string {
function formatLocation(file: SourceFile, start: number): string {
const { line, character } = getLineAndCharacterOfPosition(file, start);
const relativeFileName = displayFilename(file.fileName);
let output = "";
Expand Down
30 changes: 30 additions & 0 deletions src/getLanguageService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import fs from "node:fs";
import ts from "typescript";

export const getLanguageService = (
program: ts.Program,
overridesForTesting?: {
readFile: (path: string) => string | undefined;
fileExists: (path: string) => boolean;
directoryExists: (path: string) => boolean;
},
) => {
const languageServiceHost: ts.LanguageServiceHost = {
getCompilationSettings: () => program.getCompilerOptions(),
getScriptFileNames: () => program.getSourceFiles().map((f) => f.fileName),
getScriptVersion: () => "",
getScriptSnapshot: (fileName) => {
const sourceFile = program.getSourceFile(fileName);
if (!sourceFile) return undefined;
return ts.ScriptSnapshot.fromString(sourceFile.text);
},
getCurrentDirectory: () => process.cwd(),
getDefaultLibFileName: ts.getDefaultLibFileName,
readFile: (path, encoding) =>
overridesForTesting?.readFile(path)
?? fs.readFileSync(path, encoding as BufferEncoding),
fileExists: overridesForTesting?.fileExists ?? fs.existsSync,
directoryExists: overridesForTesting?.directoryExists,
};
return ts.createLanguageService(languageServiceHost);
};
1 change: 1 addition & 0 deletions src/getPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const getPlugin = async (
}
const result = await initRules(
() => languageService.getProgram()!,
() => languageService,
config ?? { rules: [] },
false,
);
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ import { restrictTemplateExpressions } from "./rules/restrictTemplateExpressions
import { returnAwait } from "./rules/returnAwait/returnAwait.ts";
import { strictBooleanExpressions } from "./rules/strictBooleanExpressions/strictBooleanExpressions.ts";
import { switchExhaustivenessCheck } from "./rules/switchExhaustivenessCheck/switchExhaustivenessCheck.ts";
import { unusedExport } from "./rules/unusedExport/unusedExport.ts";
import type { Config, Rule } from "./types.ts";

/* tsl-ignore core/unusedExport */
export type {
AST,
Checker,
Expand Down Expand Up @@ -96,4 +98,5 @@ export const core = createRulesSet({
returnAwait,
strictBooleanExpressions,
switchExhaustivenessCheck,
unusedExport,
});
4 changes: 4 additions & 0 deletions src/initRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const run = <T>(cb: () => T) => cb();

export const initRules = async (
getProgram: () => ts.Program,
getLanguageService: () => ts.LanguageService,
config: Config,
showTiming: boolean,
) => {
Expand Down Expand Up @@ -98,6 +99,9 @@ export const initRules = async (
get program() {
return getProgram();
},
get languageService() {
return getLanguageService();
},
get checker() {
return getProgram().getTypeChecker() as unknown as Checker;
},
Expand Down
2 changes: 1 addition & 1 deletion src/loadConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const loadConfig = async (
};
};

export const findConfigPath = (program: ts.Program) => {
const findConfigPath = (program: ts.Program) => {
let dir = program.getCurrentDirectory();
const { root } = path.parse(dir);
let configPath: string | undefined = undefined;
Expand Down
Loading