diff --git a/javascript/packages/linter/src/cli.ts b/javascript/packages/linter/src/cli.ts index cf01d4237..e8c06f2b6 100644 --- a/javascript/packages/linter/src/cli.ts +++ b/javascript/packages/linter/src/cli.ts @@ -66,9 +66,9 @@ export class CLI { } } - protected adjustPattern(pattern: string | undefined, configGlobPattern: string): string { + protected adjustPattern(pattern: string | undefined, configGlobPatterns: string[]): string { if (!pattern) { - return configGlobPattern + return configGlobPatterns.length === 1 ? configGlobPatterns[0] : `{${configGlobPatterns.join(',')}}` } const resolvedPattern = resolve(pattern) @@ -77,7 +77,15 @@ export class CLI { const stats = statSync(resolvedPattern) if (stats.isDirectory()) { - return configGlobPattern + const relativeDir = relative(this.projectPath, resolvedPattern) + + if (relativeDir) { + const scopedPatterns = configGlobPatterns.map(pattern => `${relativeDir}/${pattern}`) + + return scopedPatterns.length === 1 ? scopedPatterns[0] : `{${scopedPatterns.join(',')}}` + } + + return configGlobPatterns.length === 1 ? configGlobPatterns[0] : `{${configGlobPatterns.join(',')}}` } else if (stats.isFile()) { return relative(this.projectPath, resolvedPattern) } @@ -96,10 +104,11 @@ export class CLI { } const filesConfig = config.getFilesConfigForTool('linter') - const configGlobPattern = filesConfig.include && filesConfig.include.length > 0 - ? (filesConfig.include.length === 1 ? filesConfig.include[0] : `{${filesConfig.include.join(',')}}`) - : '**/*.html.erb' - const adjustedPattern = this.adjustPattern(pattern, configGlobPattern) + const configGlobPatterns = filesConfig.include && filesConfig.include.length > 0 + ? filesConfig.include + : ['**/*.html.erb'] + + const adjustedPattern = this.adjustPattern(pattern, configGlobPatterns) let files = await glob(adjustedPattern, { cwd: this.projectPath, diff --git a/javascript/packages/linter/src/custom-rule-loader.ts b/javascript/packages/linter/src/custom-rule-loader.ts index 182da84e9..8b2f92ce8 100644 --- a/javascript/packages/linter/src/custom-rule-loader.ts +++ b/javascript/packages/linter/src/custom-rule-loader.ts @@ -1,5 +1,5 @@ import { pathToFileURL } from "url" -import { glob } from "glob" +import { glob } from "tinyglobby" import type { RuleClass } from "./types.js" @@ -52,7 +52,7 @@ export class CustomRuleLoader { const files = await glob(pattern, { cwd: this.baseDir, absolute: true, - nodir: true + onlyFiles: true }) allFiles.push(...files) diff --git a/javascript/packages/linter/test/cli.test.ts b/javascript/packages/linter/test/cli.test.ts index b06e70ccf..91a9c2338 100644 --- a/javascript/packages/linter/test/cli.test.ts +++ b/javascript/packages/linter/test/cli.test.ts @@ -528,6 +528,90 @@ describe("CLI Output Formatting", () => { }) }) + describe("Directory Scoping (issue #1045)", () => { + const { mkdirSync, writeFileSync, rmSync, existsSync } = require("fs") + const { join } = require("path") + const tempDir = "test/fixtures/directory-scoping-test" + + function runLinterFromPath(filePath: string): { output: string, exitCode: number } { + try { + const { execSync } = require("child_process") + + const output = execSync(`bin/herb-lint ${filePath} --no-timing 2>&1`, { + encoding: "utf-8", + env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: undefined, GITHUB_ACTIONS: undefined } + }) + + return { output: output.trim(), exitCode: 0 } + } catch (error: any) { + const stderr = error.stderr ? error.stderr.toString().trim() : "" + const stdout = error.stdout ? error.stdout.toString().trim() : "" + const combined = (stdout + "\n" + stderr).trim() + + return { output: combined || stderr || stdout, exitCode: error.status } + } + } + + test("only processes files within the specified directory", () => { + try { + // Create directory structure: + // tempDir/ + // .herb.yml + // public/ + // file.html.erb (should NOT be processed) + // app/ + // views/ + // file.html.erb (should be processed) + mkdirSync(join(tempDir, "public"), { recursive: true }) + mkdirSync(join(tempDir, "app/views"), { recursive: true }) + + writeFileSync(join(tempDir, ".herb.yml"), dedent` + version: 0.8.7 + linter: + enabled: true + `) + + writeFileSync(join(tempDir, "public/file.html.erb"), `test\n`) + writeFileSync(join(tempDir, "app/views/file.html.erb"), `
clean file
\n`) + + const { output, exitCode } = runLinterFromPath(join(tempDir, "app/views")) + + expect(output).toContain("Checked 1 file") + expect(output).not.toContain("public/file.html.erb") + expect(exitCode).toBe(0) + } finally { + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true, force: true }) + } + } + }) + + test("processes all files when run from project root", () => { + try { + mkdirSync(join(tempDir, "public"), { recursive: true }) + mkdirSync(join(tempDir, "app/views"), { recursive: true }) + + writeFileSync(join(tempDir, ".herb.yml"), dedent` + version: 0.8.7 + linter: + enabled: true + `) + + writeFileSync(join(tempDir, "public/file.html.erb"), `
public file
\n`) + writeFileSync(join(tempDir, "app/views/file.html.erb"), `
views file
\n`) + + const { output, exitCode } = runLinterFromPath(tempDir) + + expect(output).toContain("Checked 2 files") + expect(exitCode).toBe(0) + } finally { + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true, force: true }) + } + } + }) + }) + describe("Custom Rules from Project Root (issue #908)", () => { const { mkdirSync, writeFileSync, rmSync, existsSync } = require("fs") const { join } = require("path")