diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index 47e3735..cbbd7d3 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -147,11 +147,19 @@ const formatFileCount = (count: number, isError = false): string => { return `${isError ? color.red(formattedCount) : formattedCount} ${count === 1 ? 'file' : 'files'}`; }; +const reportNoSupportedFiles = (patterns: string[]): void => { + const targets = (patterns.length ? patterns : ['.']) + .map((pattern) => color.cyan(JSON.stringify(pattern))) + .join(', '); + logger.error(`No supported files matched ${targets}, or all matching files were ignored.`); + process.exitCode = 2; +}; + const logFmtResult = ( result: FmtRunResult, mode: FmtMode, cwd: string, - matchedFileCount: number, + processedFileCount: number, durationSeconds: number, ): void => { let writtenCount = 0; @@ -177,12 +185,12 @@ const logFmtResult = ( return; } - const matchedFiles = formatFileCount(matchedFileCount); + const processedFiles = formatFileCount(processedFileCount); const time = prettyTime(durationSeconds); const message = writtenCount > 0 - ? `Formatted ${formatCount(writtenCount)} of ${matchedFiles} in ${time}.` - : `Checked ${matchedFiles} in ${time}. No changes needed.`; + ? `Formatted ${formatCount(writtenCount)} of ${processedFiles} in ${time}.` + : `Checked ${processedFiles} in ${time}. No changes needed.`; logger[result.exitCode === 0 ? 'success' : 'info'](message); return; } @@ -193,15 +201,15 @@ const logFmtResult = ( if (differentCount > 0) { const differentFiles = formatFileCount(differentCount, true); - const matchedFiles = formatFileCount(matchedFileCount); + const processedFiles = formatFileCount(processedFileCount); const checkOption = color.cyan('--check'); logger.error( `Formatting issues found in ${differentFiles}. Run without ${checkOption} to fix.`, ); - logger.info(`Checked ${matchedFiles} in ${prettyTime(durationSeconds)}.`); + logger.info(`Checked ${processedFiles} in ${prettyTime(durationSeconds)}.`); } else if (result.exitCode === 0) { logger.success( - `Checked ${formatFileCount(matchedFileCount)} in ${prettyTime(durationSeconds)}. No issues found.`, + `Checked ${formatFileCount(processedFileCount)} in ${prettyTime(durationSeconds)}. No issues found.`, ); } }; @@ -263,11 +271,7 @@ const runFmtCLI = async (args: string[]): Promise => { if (noErrorOnUnmatchedPattern) { return; } - const targets = (patterns.length ? patterns : ['.']) - .map((pattern) => color.cyan(JSON.stringify(pattern))) - .join(', '); - logger.error(`No supported files matched ${targets}, or all matching files were ignored.`); - process.exitCode = 2; + reportNoSupportedFiles(patterns); return; } @@ -281,8 +285,13 @@ const runFmtCLI = async (args: string[]): Promise => { maxWorkers, }); + if (result.processedFileCount === 0) { + reportNoSupportedFiles(patterns); + return; + } + const durationSeconds = (performance.now() - startTime) / 1000; - logFmtResult(result, mode, cwd, files.length, durationSeconds); + logFmtResult(result, mode, cwd, result.processedFileCount, durationSeconds); process.exitCode = result.exitCode; } catch (error) { logger.error(error); diff --git a/packages/rstack/src/fmt/runner.ts b/packages/rstack/src/fmt/runner.ts index ec3192b..aa0cc5c 100644 --- a/packages/rstack/src/fmt/runner.ts +++ b/packages/rstack/src/fmt/runner.ts @@ -9,17 +9,23 @@ import type { FmtWorkerPool } from './workerPool.ts'; /** Formats one file and reports whether its contents differ. */ type FormatFile = FmtWorkerPool['formatFile']; +type FmtFileOutcome = FmtFileResult | 'unchanged' | 'unsupported'; + +interface FmtWorkerPoolResult { + files: FmtFileResult[]; + processedFileCount: number; +} /** Converts a formatter outcome into the shared per-file result. */ const runFmtFile = async ( file: FmtFileRequest, shouldWrite: boolean, formatFile: FormatFile, -): Promise => { +): Promise => { try { const result = await formatFile(file, shouldWrite); - if (result !== 'changed') { - return; + if (result === 'unchanged' || result === 'unsupported') { + return result; } return { @@ -40,7 +46,7 @@ const runFmtFilesInWorkerPool = async ( files: FmtFileRequest[], shouldWrite: boolean, maxWorkers?: number, -): Promise => { +): Promise => { const { createFmtWorkerPool } = await import('./workerPool.ts'); const workerPool = await createFmtWorkerPool(files.length, maxWorkers); @@ -48,7 +54,21 @@ const runFmtFilesInWorkerPool = async ( const results = await Promise.all( files.map((file) => runFmtFile(file, shouldWrite, workerPool.formatFile)), ); - return results.filter((result): result is FmtFileResult => result !== undefined); + const processedFiles: FmtFileResult[] = []; + let processedFileCount = 0; + + for (const result of results) { + if (result === 'unsupported') { + continue; + } + + processedFileCount++; + if (result !== 'unchanged') { + processedFiles.push(result); + } + } + + return { files: processedFiles, processedFileCount }; } finally { await workerPool.terminate(); } @@ -77,12 +97,15 @@ const runFmtFiles = async ({ maxWorkers, }: RunFmtFilesOptions): Promise => { const shouldWrite = mode === 'write'; - const results = - files.length === 0 ? [] : await runFmtFilesInWorkerPool(files, shouldWrite, maxWorkers); + const result = + files.length === 0 + ? { files: [], processedFileCount: 0 } + : await runFmtFilesInWorkerPool(files, shouldWrite, maxWorkers); return { - files: results, - exitCode: getFmtExitCode(results), + ...result, + exitCode: + files.length > 0 && result.processedFileCount === 0 ? 2 : getFmtExitCode(result.files), }; }; diff --git a/packages/rstack/src/fmt/types.ts b/packages/rstack/src/fmt/types.ts index 111ddb4..db9b2d6 100644 --- a/packages/rstack/src/fmt/types.ts +++ b/packages/rstack/src/fmt/types.ts @@ -96,6 +96,8 @@ type FmtFileResult = SuccessfulFmtFileResult | FailedFmtFileResult; interface FmtRunResult { files: FmtFileResult[]; + /** Number of processed files, excluding files with no supported parser. */ + processedFileCount: number; /** Recommended CLI exit code. */ exitCode: FmtExitCode; } diff --git a/packages/rstack/tests/cli/fmt/index.test.ts b/packages/rstack/tests/cli/fmt/index.test.ts index 07391ce..36ee926 100644 --- a/packages/rstack/tests/cli/fmt/index.test.ts +++ b/packages/rstack/tests/cli/fmt/index.test.ts @@ -610,3 +610,41 @@ test.each(['--no-error-on-unmatched-pattern', '--noErrorOnUnmatchedPattern'])( } }, ); + +test('counts only supported files', () => { + writeProjectFile('index.ts', 'const value = 1;\n'); + writeProjectFile('notes.unknown', 'plain text'); + + const result = runFmt(['--check', 'index.ts', 'notes.unknown']); + + expect(result.status).toBe(0); + expect(normalizeDuration(result.stdout)).toBe( + 'start Checking formatting...\nsuccess Checked 1 file in . No issues found.\n', + ); + expect(result.stderr).toBe(''); +}); + +test('returns exit code 2 when all matched files are unsupported', () => { + writeProjectFile('notes.unknown', 'plain text'); + + for (const modeArgs of [[], ['--check'], ['--list-different']]) { + const result = runFmt([...modeArgs, 'notes.unknown']); + + expect(result.status).toBe(2); + expect(result.stdout).not.toContain('success'); + expect(result.stderr).toContain( + 'No supported files matched "notes.unknown", or all matching files were ignored.', + ); + expect(result.stderr).not.toContain('\n at '); + } +}); + +test('does not treat unsupported files as unmatched patterns', () => { + writeProjectFile('notes.unknown', 'plain text'); + + const result = runFmt(['--no-error-on-unmatched-pattern', 'notes.unknown']); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain('No supported files matched "notes.unknown"'); +}); diff --git a/packages/rstack/tests/fmt/runner.test.ts b/packages/rstack/tests/fmt/runner.test.ts index 385ac5e..7a31d38 100644 --- a/packages/rstack/tests/fmt/runner.test.ts +++ b/packages/rstack/tests/fmt/runner.test.ts @@ -31,6 +31,7 @@ test('does not rewrite unchanged files', async () => { expect(result).toMatchObject({ exitCode: 0, files: [], + processedFileCount: 1, }); expect(statSync(filePath).mtimeMs).toBe(mtimeMs); }); @@ -46,6 +47,7 @@ test('writes changed files', async () => { expect(result).toMatchObject({ exitCode: 0, files: [{ path: filePath, status: 'written' }], + processedFileCount: 1, }); expect(readFileSync(filePath, 'utf8')).toBe('const value = 1;\n'); }); @@ -75,6 +77,7 @@ for (const mode of ['check', 'list-different'] as const) { expect(result).toMatchObject({ exitCode: 1, files: [{ path: filePath, status: 'different' }], + processedFileCount: 1, }); expect(readFileSync(filePath, 'utf8')).toBe(source); }); @@ -96,6 +99,7 @@ test('continues after a file fails and gives errors exit-code precedence', async { path: invalidPath, status: 'error' }, { path: validPath, status: 'different' }, ], + processedFileCount: 2, }); expect(readFileSync(validPath, 'utf8')).toBe('const value=1'); }); @@ -113,7 +117,7 @@ test('omits unsupported files from the result', async () => { }, ]); - expect(result).toMatchObject({ exitCode: 0, files: [] }); + expect(result).toMatchObject({ exitCode: 2, files: [], processedFileCount: 0 }); expect(readFileSync(filePath, 'utf8')).toBe('plain text'); }); }); diff --git a/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts b/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts index 1b3871b..127beae 100644 --- a/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts +++ b/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts @@ -47,6 +47,7 @@ test('does not start the worker pool when there are no files', async () => { await expect(runFmtFiles({ files: [], mode: 'write' })).resolves.toMatchObject({ files: [], exitCode: 0, + processedFileCount: 0, }); expect(mocks.createFmtWorkerPoolCalls).toEqual([]); }); diff --git a/packages/rstack/tests/fmt/runnerWriteFailure.test.ts b/packages/rstack/tests/fmt/runnerWriteFailure.test.ts index db92d9a..a7ea7b4 100644 --- a/packages/rstack/tests/fmt/runnerWriteFailure.test.ts +++ b/packages/rstack/tests/fmt/runnerWriteFailure.test.ts @@ -40,6 +40,7 @@ test('returns an error when a file write fails', async () => { error: { message: 'file write failed' }, }, ], + processedFileCount: 1, }); expect(mocks.terminateCalls).toBe(1); });