Skip to content
Merged
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
25 changes: 25 additions & 0 deletions plans/refactor/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,31 @@
- First implementation slice for CLI execution semantics has been delivered.
- The target architecture is described in [`PLAN.md`](PLAN.md).

## Implemented in full multi-tsconfig targeting alignment slice

- Completed canonical tsconfig normalization in [`src/config.ts`](../../src/config.ts):
- all configured `domains.eslint.tsconfigPaths` are resolved to absolute paths
- unreadable or missing tsconfig files are filtered out
- resulting tsconfig list is deduplicated and deterministically sorted
- fallback to root `tsconfig.json` remains when available and readable
- Completed union-based ESLint target derivation in [`src/utils.ts`](../../src/utils.ts):
- target files are now derived from the union of all configured tsconfig includes
- excludes are merged with cross-tsconfig overlap handling
- `forceInclude` is merged on top and can suppress matching ignore entries
- file and ignore output is deduplicated and deterministically sorted
- Completed include normalization correctness in [`src/utils.ts`](../../src/utils.ts):
- include entries that already carry an extension/glob extension are preserved as-is
- only extensionless include entries are expanded with supported ESLint extensions
- malformed glob synthesis paths from blanket suffixing are eliminated
- Completed parser-project/target alignment across ESLint runtime paths:
- [`src/configs/js.ts`](../../src/configs/js.ts) continues to source parser project from resolved tsconfig list
- [`src/utils.ts`](../../src/utils.ts) now injects parser `project` override from the same resolved config used for target derivation
- [`src/domains/eslint.ts`](../../src/domains/eslint.ts) detection now derives default scope from the same multi-tsconfig union logic
- Added regression coverage for multi-tsconfig alignment:
- config normalization and missing/unreadable filtering in [`tests/config.test.ts`](../../tests/config.test.ts)
- multi-tsconfig detection and include/exclude/forceInclude behaviors in [`tests/domains/index.test.ts`](../../tests/domains/index.test.ts)
- user-visible stability with missing configured tsconfig path in [`tests/bin/lint.test.ts`](../../tests/bin/lint.test.ts)

## Implemented in config-schema cleanup + API-boundary slice

- Completed lint runtime config loading/normalization using a single explicit schema:
Expand Down
41 changes: 35 additions & 6 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import ts from 'typescript';

const MATRIXAI_LINT_CONFIG_FILENAME = 'matrixai-lint-config.json';

Expand All @@ -29,14 +30,40 @@ function stripLeadingDotSlash(value: string): string {
return value.replace(/^\.\//, '');
}

function dedupeAndSort(values: readonly string[]): string[] {
return [...new Set(values)].sort();
}

function isReadableTsconfigPath(tsconfigPath: string): boolean {
let stats: fs.Stats;
try {
stats = fs.statSync(tsconfigPath);
} catch {
return false;
}

if (!stats.isFile()) {
return false;
}

const readResult = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
return readResult.error == null;
}

function sanitizeTsconfigPaths(rawValue: unknown, root: string): string[] {
return toStringArray(rawValue)
.map((tsconfigPath) => path.resolve(root, tsconfigPath))
.filter((tsconfigPath) => fs.existsSync(tsconfigPath));
return dedupeAndSort(
toStringArray(rawValue)
.map((tsconfigPath) => path.resolve(root, tsconfigPath))
.filter((tsconfigPath) => isReadableTsconfigPath(tsconfigPath)),
);
}

function sanitizeForceInclude(rawValue: unknown): string[] {
return toStringArray(rawValue).map((glob) => stripLeadingDotSlash(glob));
return dedupeAndSort(
toStringArray(rawValue)
.map((glob) => stripLeadingDotSlash(glob))
.filter((glob) => glob.length > 0),
);
}

function normalizeLintConfig({
Expand All @@ -63,19 +90,21 @@ function normalizeLintConfig({
? rawDomains.eslint
: ({} as Record<string, unknown>);

const tsconfigPaths = sanitizeTsconfigPaths(
let tsconfigPaths = sanitizeTsconfigPaths(
rawEslintDomain.tsconfigPaths,
resolvedRoot,
);
const forceInclude = sanitizeForceInclude(rawEslintDomain.forceInclude);

if (tsconfigPaths.length === 0) {
const rootTsconfigPath = path.join(resolvedRoot, 'tsconfig.json');
if (fs.existsSync(rootTsconfigPath)) {
if (isReadableTsconfigPath(rootTsconfigPath)) {
tsconfigPaths.push(rootTsconfigPath);
}
}

tsconfigPaths = dedupeAndSort(tsconfigPaths);

return {
version: 2,
root: resolvedRoot,
Expand Down
33 changes: 29 additions & 4 deletions src/domains/eslint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
resolveSearchRootsFromPatterns,
} from './files.js';
import * as utils from '../utils.js';
import { resolveLintConfig } from '../config.js';

const ESLINT_FILE_EXTENSIONS = [
'.js',
Expand All @@ -20,15 +21,39 @@ const ESLINT_FILE_EXTENSIONS = [

const DEFAULT_ESLINT_SEARCH_ROOTS = ['./src', './scripts', './tests'];

function resolveESLintDetectionPatterns(
eslintPatterns: readonly string[] | undefined,
): string[] {
if (eslintPatterns != null && eslintPatterns.length > 0) {
return [...eslintPatterns];
}

const resolvedConfig = resolveLintConfig();
const { tsconfigPaths, forceInclude } = resolvedConfig.domains.eslint;

if (tsconfigPaths.length === 0) {
return DEFAULT_ESLINT_SEARCH_ROOTS;
}

const { files } = utils.buildPatterns(
tsconfigPaths,
forceInclude,
process.cwd(),
resolvedConfig.root,
);
if (files.length === 0) {
return DEFAULT_ESLINT_SEARCH_ROOTS;
}

return files;
}

function createESLintDomainPlugin(): LintDomainPlugin {
return {
domain: 'eslint',
description: 'Lint JavaScript/TypeScript/JSON files with ESLint.',
detect: ({ eslintPatterns }) => {
const patterns =
eslintPatterns != null && eslintPatterns.length > 0
? eslintPatterns
: DEFAULT_ESLINT_SEARCH_ROOTS;
const patterns = resolveESLintDetectionPatterns(eslintPatterns);
const searchRoots = resolveSearchRootsFromPatterns(patterns);
const matchedFiles = collectFilesByExtensions(
searchRoots,
Expand Down
Loading
Loading