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
7 changes: 3 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@matrixai/lint",
"version": "0.4.2",
"version": "0.4.3",
"author": "Roger Qiu",
"description": "Org wide custom eslint rules",
"license": "Apache-2.0",
Expand Down Expand Up @@ -59,6 +59,7 @@
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-tailwindcss": "^3.18.0",
"globals": "^16.2.0",
"minimatch": "^10.0.1",
"prettier": "^3.0.0"
},
"devDependencies": {
Expand All @@ -69,7 +70,6 @@
"jest": "^29.6.2",
"jest-extended": "^4.0.2",
"jest-junit": "^16.0.0",
"minimatch": "^10.0.1",
"shx": "^0.3.4",
"tsx": "^3.12.7",
"typedoc": "^0.24.8",
Expand Down
37 changes: 26 additions & 11 deletions src/domains/eslint.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { LintDomainPlugin } from './engine.js';
import {
collectFilesByExtensions,
relativizeFiles,
resolveFilesFromPatterns,
resolveSearchRootsFromPatterns,
} from './files.js';
import * as utils from '../utils.js';
Expand Down Expand Up @@ -53,23 +52,35 @@ function createESLintDomainPlugin(): LintDomainPlugin {
domain: 'eslint',
description: 'Lint JavaScript/TypeScript/JSON files with ESLint.',
detect: ({ eslintPatterns }) => {
const patterns = resolveESLintDetectionPatterns(eslintPatterns);
const searchRoots = resolveSearchRootsFromPatterns(patterns);
const matchedFiles = collectFilesByExtensions(
searchRoots,
if (eslintPatterns != null && eslintPatterns.length > 0) {
const searchRoots = resolveSearchRootsFromPatterns(eslintPatterns);

return {
relevant: searchRoots.length > 0,
relevanceReason:
searchRoots.length > 0
? undefined
: 'No ESLint-supported files matched in effective scope.',
available: true,
availabilityKind: 'required' as const,
};
}

const detectionPatterns = resolveESLintDetectionPatterns(eslintPatterns);
const matchedFiles = resolveFilesFromPatterns(
detectionPatterns,
ESLINT_FILE_EXTENSIONS,
);
const matchedRelativeFiles = relativizeFiles(matchedFiles);

return {
relevant: matchedRelativeFiles.length > 0,
relevant: matchedFiles.length > 0,
relevanceReason:
matchedRelativeFiles.length > 0
matchedFiles.length > 0
? undefined
: 'No ESLint-supported files matched in effective scope.',
available: true,
availabilityKind: 'required' as const,
matchedFiles: matchedRelativeFiles,
matchedFiles,
};
},
run: async ({
Expand All @@ -85,11 +96,15 @@ function createESLintDomainPlugin(): LintDomainPlugin {
}

try {
const explicitPatterns =
eslintPatterns != null && eslintPatterns.length > 0
? resolveFilesFromPatterns(eslintPatterns, ESLINT_FILE_EXTENSIONS)
: undefined;
const hadLintingErrors = await utils.runESLint({
fix,
logger,
configPath: chosenConfig,
explicitGlobs: eslintPatterns,
explicitGlobs: explicitPatterns,
});

return { hadFailure: hadLintingErrors };
Expand Down
131 changes: 127 additions & 4 deletions src/domains/files.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,58 @@
import path from 'node:path';
import process from 'node:process';
import fs from 'node:fs';
import { minimatch } from 'minimatch';

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

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

function normalizePathForGlob(value: string): string {
return value.replace(/\\/g, '/').replace(/^\.\//, '');
}

function normalizePatternForSearchRoot(pattern: string): string {
return pattern.trim().replace(/\\/g, '/');
}

function toPosixRelativePath(filePath: string, cwd = process.cwd()): string {
const relativePath = path.relative(cwd, filePath).split(path.sep).join('/');
if (relativePath === '') {
return '.';
}
return relativePath;
}

function normalizePatternForMatching(
pattern: string,
cwd = process.cwd(),
): string {
const normalizedPattern = normalizePathForGlob(pattern.trim());
if (normalizedPattern.length === 0) {
return '';
}

const platformPattern = normalizedPattern.split('/').join(path.sep);
const absolutePattern = path.isAbsolute(platformPattern)
? platformPattern
: path.resolve(cwd, platformPattern);

return toPosixRelativePath(absolutePattern, cwd);
}

function isGlobPattern(value: string): boolean {
return GLOB_META_PATTERN.test(value);
}

function patternToSearchRoot(pattern: string, cwd = process.cwd()): string {
if (!isGlobPattern(pattern)) {
return path.resolve(cwd, pattern);
const normalizedPattern = normalizePatternForSearchRoot(pattern);

if (!isGlobPattern(normalizedPattern)) {
return path.resolve(cwd, normalizedPattern);
}

const normalizedPattern = pattern.split('/').join(path.sep);
const segments = normalizedPattern
const platformPattern = normalizedPattern.split('/').join(path.sep);
const segments = platformPattern
.split(path.sep)
.filter((segment) => segment.length > 0);
const rootSegments: string[] = [];
Expand Down Expand Up @@ -105,6 +141,92 @@ function collectFilesByExtensions(
return [...matchedFiles].sort();
}

function resolveFilesFromPatterns(
patterns: readonly string[],
extensions: readonly string[],
cwd = process.cwd(),
): string[] {
const normalizedPatterns = [...new Set(patterns)]
.map((pattern) => pattern.trim())
.filter((pattern) => pattern.length > 0);

if (normalizedPatterns.length === 0) {
return [];
}

const extensionSet = new Set(
extensions.map((extension) => extension.toLowerCase()),
);
const matchedFiles = new Set<string>();
const literalFiles = new Set<string>();
const literalDirectories = new Set<string>();
const globPatterns: string[] = [];

for (const pattern of normalizedPatterns) {
const platformPattern = pattern.replace(/\//g, path.sep);
const absolutePath = path.isAbsolute(platformPattern)
? platformPattern
: path.resolve(cwd, platformPattern);
let stats: fs.Stats | undefined;

try {
stats = fs.statSync(absolutePath);
} catch {
stats = undefined;
}

if (stats?.isFile()) {
literalFiles.add(absolutePath);
continue;
}

if (stats?.isDirectory()) {
literalDirectories.add(absolutePath);
continue;
}

if (isGlobPattern(pattern)) {
globPatterns.push(pattern);
continue;
}
}

for (const literalFile of literalFiles) {
const extension = path.extname(literalFile).toLowerCase();
if (extensionSet.has(extension)) {
matchedFiles.add(literalFile);
}
}

for (const literalDirectory of literalDirectories) {
const files = collectFilesByExtensions([literalDirectory], extensions);
files.forEach((file) => matchedFiles.add(file));
}

if (globPatterns.length > 0) {
const globRoots = resolveSearchRootsFromPatterns(globPatterns, cwd);
const globCandidates = collectFilesByExtensions(globRoots, extensions);
const normalizedGlobPatterns = globPatterns
.map((pattern) => normalizePatternForMatching(pattern, cwd))
.filter((pattern) => pattern.length > 0);

for (const candidate of globCandidates) {
const relativeCandidatePath = toPosixRelativePath(candidate, cwd);
if (
normalizedGlobPatterns.some((pattern) =>
minimatch(relativeCandidatePath, pattern, {
dot: true,
}),
)
) {
matchedFiles.add(candidate);
}
}
}

return relativizeFiles([...matchedFiles].sort(), cwd);
}

function relativizeFiles(
files: readonly string[],
cwd = process.cwd(),
Expand All @@ -121,5 +243,6 @@ function relativizeFiles(
export {
resolveSearchRootsFromPatterns,
collectFilesByExtensions,
resolveFilesFromPatterns,
relativizeFiles,
};
31 changes: 14 additions & 17 deletions src/domains/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import process from 'node:process';
import childProcess from 'node:child_process';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import {
collectFilesByExtensions,
relativizeFiles,
resolveSearchRootsFromPatterns,
} from './files.js';
import { resolveFilesFromPatterns } from './files.js';

const platform = os.platform();
const MARKDOWN_FILE_EXTENSIONS = ['.md', '.mdx'] as const;
Expand All @@ -21,14 +17,11 @@ const DEFAULT_MARKDOWN_SEARCH_ROOTS = [
'./docs',
];

function collectMarkdownFilesFromScope(
searchRoots: readonly string[],
): string[] {
const matchedFiles = collectFilesByExtensions(
searchRoots,
function collectMarkdownFilesFromScope(patterns: readonly string[]): string[] {
const matchedRelativeFiles = resolveFilesFromPatterns(
patterns,
MARKDOWN_FILE_EXTENSIONS,
);
const matchedRelativeFiles = relativizeFiles(matchedFiles);

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

function resolveMarkdownPatterns(
markdownPatterns: readonly string[] | undefined,
): string[] {
return markdownPatterns != null && markdownPatterns.length > 0
? [...markdownPatterns]
: [...DEFAULT_MARKDOWN_SEARCH_ROOTS];
}

function createMarkdownDomainPlugin({
prettierConfigPath,
}: {
Expand All @@ -48,12 +49,8 @@ function createMarkdownDomainPlugin({
domain: 'markdown',
description: 'Format and check Markdown/MDX files with Prettier.',
detect: ({ markdownPatterns }) => {
const searchPatterns =
markdownPatterns != null && markdownPatterns.length > 0
? markdownPatterns
: DEFAULT_MARKDOWN_SEARCH_ROOTS;
const searchRoots = resolveSearchRootsFromPatterns(searchPatterns);
const matchedFiles = collectMarkdownFilesFromScope(searchRoots);
const patterns = resolveMarkdownPatterns(markdownPatterns);
const matchedFiles = collectMarkdownFilesFromScope(patterns);

return {
relevant: matchedFiles.length > 0,
Expand Down
Loading
Loading