Skip to content

Commit a69507d

Browse files
committed
feat(domains): unify target glob resolution across lint domains
- standardize explicit target expansion for eslint shell and markdown paths - enable eslint content cache for faster repeated lint runs - align detection and execution behavior and extend domain tests
1 parent 2d89c7c commit a69507d

8 files changed

Lines changed: 411 additions & 51 deletions

File tree

package-lock.json

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"eslint-plugin-react-hooks": "^5.1.0",
6060
"eslint-plugin-tailwindcss": "^3.18.0",
6161
"globals": "^16.2.0",
62+
"minimatch": "^10.0.1",
6263
"prettier": "^3.0.0"
6364
},
6465
"devDependencies": {
@@ -69,7 +70,6 @@
6970
"jest": "^29.6.2",
7071
"jest-extended": "^4.0.2",
7172
"jest-junit": "^16.0.0",
72-
"minimatch": "^10.0.1",
7373
"shx": "^0.3.4",
7474
"tsx": "^3.12.7",
7575
"typedoc": "^0.24.8",

src/domains/eslint.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { LintDomainPlugin } from './engine.js';
22
import {
3-
collectFilesByExtensions,
4-
relativizeFiles,
3+
resolveFilesFromPatterns,
54
resolveSearchRootsFromPatterns,
65
} from './files.js';
76
import * as utils from '../utils.js';
@@ -53,23 +52,35 @@ function createESLintDomainPlugin(): LintDomainPlugin {
5352
domain: 'eslint',
5453
description: 'Lint JavaScript/TypeScript/JSON files with ESLint.',
5554
detect: ({ eslintPatterns }) => {
56-
const patterns = resolveESLintDetectionPatterns(eslintPatterns);
57-
const searchRoots = resolveSearchRootsFromPatterns(patterns);
58-
const matchedFiles = collectFilesByExtensions(
59-
searchRoots,
55+
if (eslintPatterns != null && eslintPatterns.length > 0) {
56+
const searchRoots = resolveSearchRootsFromPatterns(eslintPatterns);
57+
58+
return {
59+
relevant: searchRoots.length > 0,
60+
relevanceReason:
61+
searchRoots.length > 0
62+
? undefined
63+
: 'No ESLint-supported files matched in effective scope.',
64+
available: true,
65+
availabilityKind: 'required' as const,
66+
};
67+
}
68+
69+
const detectionPatterns = resolveESLintDetectionPatterns(eslintPatterns);
70+
const matchedFiles = resolveFilesFromPatterns(
71+
detectionPatterns,
6072
ESLINT_FILE_EXTENSIONS,
6173
);
62-
const matchedRelativeFiles = relativizeFiles(matchedFiles);
6374

6475
return {
65-
relevant: matchedRelativeFiles.length > 0,
76+
relevant: matchedFiles.length > 0,
6677
relevanceReason:
67-
matchedRelativeFiles.length > 0
78+
matchedFiles.length > 0
6879
? undefined
6980
: 'No ESLint-supported files matched in effective scope.',
7081
available: true,
7182
availabilityKind: 'required' as const,
72-
matchedFiles: matchedRelativeFiles,
83+
matchedFiles,
7384
};
7485
},
7586
run: async ({
@@ -85,11 +96,15 @@ function createESLintDomainPlugin(): LintDomainPlugin {
8596
}
8697

8798
try {
99+
const explicitPatterns =
100+
eslintPatterns != null && eslintPatterns.length > 0
101+
? resolveFilesFromPatterns(eslintPatterns, ESLINT_FILE_EXTENSIONS)
102+
: undefined;
88103
const hadLintingErrors = await utils.runESLint({
89104
fix,
90105
logger,
91106
configPath: chosenConfig,
92-
explicitGlobs: eslintPatterns,
107+
explicitGlobs: explicitPatterns,
93108
});
94109

95110
return { hadFailure: hadLintingErrors };

src/domains/files.ts

Lines changed: 127 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,58 @@
11
import path from 'node:path';
22
import process from 'node:process';
33
import fs from 'node:fs';
4+
import { minimatch } from 'minimatch';
45

56
const GLOB_META_PATTERN = /[*?[\]{}()!+@]/;
67

78
const EXCLUDED_DIR_NAMES = new Set(['.git', 'node_modules', 'dist']);
89

10+
function normalizePathForGlob(value: string): string {
11+
return value.replace(/\\/g, '/').replace(/^\.\//, '');
12+
}
13+
14+
function normalizePatternForSearchRoot(pattern: string): string {
15+
return pattern.trim().replace(/\\/g, '/');
16+
}
17+
18+
function toPosixRelativePath(filePath: string, cwd = process.cwd()): string {
19+
const relativePath = path.relative(cwd, filePath).split(path.sep).join('/');
20+
if (relativePath === '') {
21+
return '.';
22+
}
23+
return relativePath;
24+
}
25+
26+
function normalizePatternForMatching(
27+
pattern: string,
28+
cwd = process.cwd(),
29+
): string {
30+
const normalizedPattern = normalizePathForGlob(pattern.trim());
31+
if (normalizedPattern.length === 0) {
32+
return '';
33+
}
34+
35+
const platformPattern = normalizedPattern.split('/').join(path.sep);
36+
const absolutePattern = path.isAbsolute(platformPattern)
37+
? platformPattern
38+
: path.resolve(cwd, platformPattern);
39+
40+
return toPosixRelativePath(absolutePattern, cwd);
41+
}
42+
943
function isGlobPattern(value: string): boolean {
1044
return GLOB_META_PATTERN.test(value);
1145
}
1246

1347
function patternToSearchRoot(pattern: string, cwd = process.cwd()): string {
14-
if (!isGlobPattern(pattern)) {
15-
return path.resolve(cwd, pattern);
48+
const normalizedPattern = normalizePatternForSearchRoot(pattern);
49+
50+
if (!isGlobPattern(normalizedPattern)) {
51+
return path.resolve(cwd, normalizedPattern);
1652
}
1753

18-
const normalizedPattern = pattern.split('/').join(path.sep);
19-
const segments = normalizedPattern
54+
const platformPattern = normalizedPattern.split('/').join(path.sep);
55+
const segments = platformPattern
2056
.split(path.sep)
2157
.filter((segment) => segment.length > 0);
2258
const rootSegments: string[] = [];
@@ -105,6 +141,92 @@ function collectFilesByExtensions(
105141
return [...matchedFiles].sort();
106142
}
107143

144+
function resolveFilesFromPatterns(
145+
patterns: readonly string[],
146+
extensions: readonly string[],
147+
cwd = process.cwd(),
148+
): string[] {
149+
const normalizedPatterns = [...new Set(patterns)]
150+
.map((pattern) => pattern.trim())
151+
.filter((pattern) => pattern.length > 0);
152+
153+
if (normalizedPatterns.length === 0) {
154+
return [];
155+
}
156+
157+
const extensionSet = new Set(
158+
extensions.map((extension) => extension.toLowerCase()),
159+
);
160+
const matchedFiles = new Set<string>();
161+
const literalFiles = new Set<string>();
162+
const literalDirectories = new Set<string>();
163+
const globPatterns: string[] = [];
164+
165+
for (const pattern of normalizedPatterns) {
166+
const platformPattern = pattern.replace(/\//g, path.sep);
167+
const absolutePath = path.isAbsolute(platformPattern)
168+
? platformPattern
169+
: path.resolve(cwd, platformPattern);
170+
let stats: fs.Stats | undefined;
171+
172+
try {
173+
stats = fs.statSync(absolutePath);
174+
} catch {
175+
stats = undefined;
176+
}
177+
178+
if (stats?.isFile()) {
179+
literalFiles.add(absolutePath);
180+
continue;
181+
}
182+
183+
if (stats?.isDirectory()) {
184+
literalDirectories.add(absolutePath);
185+
continue;
186+
}
187+
188+
if (isGlobPattern(pattern)) {
189+
globPatterns.push(pattern);
190+
continue;
191+
}
192+
}
193+
194+
for (const literalFile of literalFiles) {
195+
const extension = path.extname(literalFile).toLowerCase();
196+
if (extensionSet.has(extension)) {
197+
matchedFiles.add(literalFile);
198+
}
199+
}
200+
201+
for (const literalDirectory of literalDirectories) {
202+
const files = collectFilesByExtensions([literalDirectory], extensions);
203+
files.forEach((file) => matchedFiles.add(file));
204+
}
205+
206+
if (globPatterns.length > 0) {
207+
const globRoots = resolveSearchRootsFromPatterns(globPatterns, cwd);
208+
const globCandidates = collectFilesByExtensions(globRoots, extensions);
209+
const normalizedGlobPatterns = globPatterns
210+
.map((pattern) => normalizePatternForMatching(pattern, cwd))
211+
.filter((pattern) => pattern.length > 0);
212+
213+
for (const candidate of globCandidates) {
214+
const relativeCandidatePath = toPosixRelativePath(candidate, cwd);
215+
if (
216+
normalizedGlobPatterns.some((pattern) =>
217+
minimatch(relativeCandidatePath, pattern, {
218+
dot: true,
219+
}),
220+
)
221+
) {
222+
matchedFiles.add(candidate);
223+
}
224+
}
225+
}
226+
227+
return relativizeFiles([...matchedFiles].sort(), cwd);
228+
}
229+
108230
function relativizeFiles(
109231
files: readonly string[],
110232
cwd = process.cwd(),
@@ -121,5 +243,6 @@ function relativizeFiles(
121243
export {
122244
resolveSearchRootsFromPatterns,
123245
collectFilesByExtensions,
246+
resolveFilesFromPatterns,
124247
relativizeFiles,
125248
};

src/domains/markdown.ts

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@ import process from 'node:process';
44
import childProcess from 'node:child_process';
55
import fs from 'node:fs';
66
import { createRequire } from 'node:module';
7-
import {
8-
collectFilesByExtensions,
9-
relativizeFiles,
10-
resolveSearchRootsFromPatterns,
11-
} from './files.js';
7+
import { resolveFilesFromPatterns } from './files.js';
128

139
const platform = os.platform();
1410
const MARKDOWN_FILE_EXTENSIONS = ['.md', '.mdx'] as const;
@@ -21,14 +17,11 @@ const DEFAULT_MARKDOWN_SEARCH_ROOTS = [
2117
'./docs',
2218
];
2319

24-
function collectMarkdownFilesFromScope(
25-
searchRoots: readonly string[],
26-
): string[] {
27-
const matchedFiles = collectFilesByExtensions(
28-
searchRoots,
20+
function collectMarkdownFilesFromScope(patterns: readonly string[]): string[] {
21+
const matchedRelativeFiles = resolveFilesFromPatterns(
22+
patterns,
2923
MARKDOWN_FILE_EXTENSIONS,
3024
);
31-
const matchedRelativeFiles = relativizeFiles(matchedFiles);
3225

3326
for (const rootFile of [...DEFAULT_MARKDOWN_ROOT_FILES].reverse()) {
3427
if (!matchedRelativeFiles.includes(rootFile) && fs.existsSync(rootFile)) {
@@ -39,6 +32,14 @@ function collectMarkdownFilesFromScope(
3932
return matchedRelativeFiles;
4033
}
4134

35+
function resolveMarkdownPatterns(
36+
markdownPatterns: readonly string[] | undefined,
37+
): string[] {
38+
return markdownPatterns != null && markdownPatterns.length > 0
39+
? [...markdownPatterns]
40+
: [...DEFAULT_MARKDOWN_SEARCH_ROOTS];
41+
}
42+
4243
function createMarkdownDomainPlugin({
4344
prettierConfigPath,
4445
}: {
@@ -48,12 +49,8 @@ function createMarkdownDomainPlugin({
4849
domain: 'markdown',
4950
description: 'Format and check Markdown/MDX files with Prettier.',
5051
detect: ({ markdownPatterns }) => {
51-
const searchPatterns =
52-
markdownPatterns != null && markdownPatterns.length > 0
53-
? markdownPatterns
54-
: DEFAULT_MARKDOWN_SEARCH_ROOTS;
55-
const searchRoots = resolveSearchRootsFromPatterns(searchPatterns);
56-
const matchedFiles = collectMarkdownFilesFromScope(searchRoots);
52+
const patterns = resolveMarkdownPatterns(markdownPatterns);
53+
const matchedFiles = collectMarkdownFilesFromScope(patterns);
5754

5855
return {
5956
relevant: matchedFiles.length > 0,

src/domains/shell.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@ import type { LintDomainPlugin } from './engine.js';
22
import os from 'node:os';
33
import process from 'node:process';
44
import childProcess from 'node:child_process';
5-
import {
6-
collectFilesByExtensions,
7-
relativizeFiles,
8-
resolveSearchRootsFromPatterns,
9-
} from './files.js';
5+
import { resolveFilesFromPatterns } from './files.js';
106
import * as utils from '../utils.js';
117

128
const platform = os.platform();
139

1410
const SHELL_FILE_EXTENSIONS = ['.sh'] as const;
1511

12+
function resolveShellPatterns(
13+
shellPatterns: readonly string[] | undefined,
14+
defaultSearchRoots: readonly string[],
15+
): string[] {
16+
return shellPatterns != null && shellPatterns.length > 0
17+
? [...shellPatterns]
18+
: [...defaultSearchRoots];
19+
}
20+
1621
function createShellDomainPlugin({
1722
defaultSearchRoots,
1823
}: {
@@ -22,30 +27,25 @@ function createShellDomainPlugin({
2227
domain: 'shell',
2328
description: 'Lint shell scripts with shellcheck when available.',
2429
detect: ({ shellPatterns }) => {
25-
const patterns =
26-
shellPatterns != null && shellPatterns.length > 0
27-
? shellPatterns
28-
: [...defaultSearchRoots];
29-
const searchRoots = resolveSearchRootsFromPatterns(patterns);
30-
const matchedFiles = collectFilesByExtensions(
31-
searchRoots,
30+
const patterns = resolveShellPatterns(shellPatterns, defaultSearchRoots);
31+
const matchedFiles = resolveFilesFromPatterns(
32+
patterns,
3233
SHELL_FILE_EXTENSIONS,
3334
);
34-
const matchedRelativeFiles = relativizeFiles(matchedFiles);
3535
const hasShellcheck = utils.commandExists('shellcheck');
3636

3737
return {
38-
relevant: matchedRelativeFiles.length > 0,
38+
relevant: matchedFiles.length > 0,
3939
relevanceReason:
40-
matchedRelativeFiles.length > 0
40+
matchedFiles.length > 0
4141
? undefined
4242
: 'No shell script files matched in effective scope.',
4343
available: hasShellcheck,
4444
availabilityKind: 'optional' as const,
4545
unavailableReason: hasShellcheck
4646
? undefined
4747
: 'shellcheck not found in environment.',
48-
matchedFiles: matchedRelativeFiles,
48+
matchedFiles,
4949
};
5050
},
5151
run: ({ logger }, detection) => {

0 commit comments

Comments
 (0)