Skip to content

Commit 6aff461

Browse files
authored
Add --check option to format command (#1962)
* add: --check option to format command * add: return exit code when error occured during formatting * add: test for --check option of format command * fix: output error to stderr not stdout * add: error message when error occured during formatting * refactor: separate test for format
1 parent e00dde0 commit 6aff461

File tree

3 files changed

+166
-12
lines changed

3 files changed

+166
-12
lines changed

cli/console.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,6 @@ export function printExecutedAction(
234234
const jobIdSuffix = jobIds.length > 0 ? ` (jobId: ${jobIds.join(", ")})` : "";
235235
switch (executedAction.status) {
236236
case dataform.ActionResult.ExecutionStatus.SUCCESSFUL: {
237-
238237
switch (executionAction.type) {
239238
case "table": {
240239
writeStdOut(
@@ -250,15 +249,21 @@ export function printExecutedAction(
250249
writeStdOut(
251250
`${successOutput(
252251
`Assertion ${dryRun ? "dry run success" : "passed"}: `
253-
)} ${assertionString(executionAction.target, executionAction.tasks.length === 0)}${jobIdSuffix}`
252+
)} ${assertionString(
253+
executionAction.target,
254+
executionAction.tasks.length === 0
255+
)}${jobIdSuffix}`
254256
);
255257
return;
256258
}
257259
case "operation": {
258260
writeStdOut(
259261
`${successOutput(
260262
`Operation ${dryRun ? "dry run success" : "completed successfully"}: `
261-
)} ${operationString(executionAction.target, executionAction.tasks.length === 0)}${jobIdSuffix}`
263+
)} ${operationString(
264+
executionAction.target,
265+
executionAction.tasks.length === 0
266+
)}${jobIdSuffix}`
262267
);
263268
return;
264269
}
@@ -371,15 +376,24 @@ export function printFormatFilesResult(
371376
formatResults: Array<{
372377
filename: string;
373378
err?: Error;
379+
needsFormatting?: boolean;
374380
}>
375381
) {
376382
const sorted = formatResults.sort((a, b) => a.filename.localeCompare(b.filename));
377-
const successfulFormatResults = sorted.filter(result => !result.err);
383+
const successfulFormatResults = sorted.filter(result => !result.err && !result.needsFormatting);
384+
const needsFormattingResults = sorted.filter(result => !result.err && result.needsFormatting);
378385
const failedFormatResults = sorted.filter(result => !!result.err);
386+
379387
if (successfulFormatResults.length > 0) {
380388
printSuccess("Successfully formatted:");
381389
successfulFormatResults.forEach(result => writeStdOut(result.filename, 1));
382390
}
391+
392+
if (needsFormattingResults.length > 0) {
393+
printError("Files that need formatting:");
394+
needsFormattingResults.forEach(result => writeStdErr(result.filename, 1));
395+
}
396+
383397
if (failedFormatResults.length > 0) {
384398
printError("Errors encountered during formatting:");
385399
failedFormatResults.forEach(result =>

cli/index.ts

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ const watchOptionName = "watch";
171171

172172
const dryRunOptionName = "dry-run";
173173
const runTestsOptionName = "run-tests";
174+
const checkOptionName = "check";
174175

175176
const actionRetryLimitName = "action-retry-limit";
176177

@@ -586,7 +587,17 @@ export function runCli() {
586587
format: `format [${projectDirMustExistOption.name}]`,
587588
description: "Format the dataform project's files.",
588589
positionalOptions: [projectDirMustExistOption],
589-
options: [actionsOption],
590+
options: [
591+
actionsOption,
592+
{
593+
name: checkOptionName,
594+
option: {
595+
describe: "Check if files are formatted correctly without modifying them.",
596+
type: "boolean",
597+
default: false
598+
}
599+
}
600+
],
590601
processFn: async argv => {
591602
let actions = ["{definitions,includes}/**/*.{js,sqlx}"];
592603
if (actionsOption.name in argv && argv[actionsOption.name].length > 0) {
@@ -597,15 +608,35 @@ export function runCli() {
597608
glob.sync(action, { cwd: argv[projectDirMustExistOption.name] })
598609
)
599610
.flat();
600-
const results: Array<{ filename: string; err?: Error }> = await Promise.all(
611+
612+
const isCheckMode = argv[checkOptionName];
613+
const results: Array<{
614+
filename: string;
615+
err?: Error;
616+
needsFormatting?: boolean;
617+
}> = await Promise.all(
601618
filenames.map(async (filename: string) => {
602619
try {
603-
await formatFile(path.resolve(argv[projectDirMustExistOption.name], filename), {
604-
overwriteFile: true
605-
});
606-
return {
607-
filename
608-
};
620+
const filePath = path.resolve(argv[projectDirMustExistOption.name], filename);
621+
if (isCheckMode) {
622+
// In check mode, we don't modify files, just check if they need formatting
623+
const fileContent = fs.readFileSync(filePath).toString();
624+
const formattedContent = await formatFile(filePath, {
625+
overwriteFile: false
626+
});
627+
return {
628+
filename,
629+
needsFormatting: fileContent !== formattedContent
630+
};
631+
} else {
632+
// Normal formatting mode
633+
await formatFile(filePath, {
634+
overwriteFile: true
635+
});
636+
return {
637+
filename
638+
};
639+
}
609640
} catch (e) {
610641
return {
611642
filename,
@@ -614,7 +645,28 @@ export function runCli() {
614645
}
615646
})
616647
);
648+
617649
printFormatFilesResult(results);
650+
651+
// Return error code if there are any formatting errors
652+
const failedFormatResults = results.filter(result => !!result.err);
653+
if (failedFormatResults.length > 0) {
654+
printError(`${failedFormatResults.length} file(s) failed to format.`);
655+
return 1;
656+
}
657+
658+
// In check mode, return an error code if any files need formatting
659+
if (isCheckMode) {
660+
const filesNeedingFormatting = results.filter(result => result.needsFormatting);
661+
if (filesNeedingFormatting.length > 0) {
662+
printError(
663+
`${filesNeedingFormatting.length} file(s) would be reformatted. Run the format command without --check to update.`
664+
);
665+
return 1;
666+
}
667+
printSuccess("All files are formatted correctly!");
668+
}
669+
618670
return 0;
619671
}
620672
}

cli/index_test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,4 +304,92 @@ select 1 as \${dataform.projectConfig.vars.testVar2}
304304
warehouseState: {}
305305
});
306306
});
307+
308+
test("test for format command", async () => {
309+
const projectDir = tmpDirFixture.createNewTmpDir();
310+
const npmCacheDir = tmpDirFixture.createNewTmpDir();
311+
const workflowSettingsPath = path.join(projectDir, "workflow_settings.yaml");
312+
const packageJsonPath = path.join(projectDir, "package.json");
313+
314+
// Initialize a project using the CLI, don't install packages.
315+
await getProcessResult(
316+
execFile(nodePath, [cliEntryPointPath, "init", projectDir, "dataform-open-source", "US"])
317+
);
318+
319+
// Install packages manually to get around bazel read-only sandbox issues.
320+
const workflowSettings = dataform.WorkflowSettings.create(
321+
loadYaml(fs.readFileSync(workflowSettingsPath, "utf8"))
322+
);
323+
delete workflowSettings.dataformCoreVersion;
324+
fs.writeFileSync(workflowSettingsPath, dumpYaml(workflowSettings));
325+
fs.writeFileSync(
326+
packageJsonPath,
327+
`{
328+
"dependencies":{
329+
"@dataform/core": "${version}"
330+
}
331+
}`
332+
);
333+
await getProcessResult(
334+
execFile(npmPath, [
335+
"install",
336+
"--prefix",
337+
projectDir,
338+
"--cache",
339+
npmCacheDir,
340+
corePackageTarPath
341+
])
342+
);
343+
344+
// Create a correctly formatted file
345+
const formattedFilePath = path.join(projectDir, "definitions", "formatted.sqlx");
346+
fs.ensureFileSync(formattedFilePath);
347+
fs.writeFileSync(
348+
formattedFilePath,
349+
`
350+
config {
351+
type: "table"
352+
}
353+
354+
SELECT
355+
1 AS test
356+
`
357+
);
358+
359+
// Create a file that needs formatting (extra spaces, inconsistent indentation)
360+
const unformattedFilePath = path.join(projectDir, "definitions", "unformatted.sqlx");
361+
fs.ensureFileSync(unformattedFilePath);
362+
fs.writeFileSync(
363+
unformattedFilePath,
364+
`
365+
config { type: "table" }
366+
SELECT 1 as test
367+
`
368+
);
369+
370+
// Test with --check flag on a project with files needing formatting
371+
const beforeFormatCheckResult = await getProcessResult(
372+
execFile(nodePath, [cliEntryPointPath, "format", projectDir, "--check"])
373+
);
374+
375+
// Should exit with code 1 when files need formatting
376+
expect(beforeFormatCheckResult.exitCode).equals(1);
377+
expect(beforeFormatCheckResult.stderr).contains("Files that need formatting");
378+
expect(beforeFormatCheckResult.stderr).contains("unformatted.sqlx");
379+
380+
// Format the files (without check flag)
381+
const formatCheckResult = await getProcessResult(
382+
execFile(nodePath, [cliEntryPointPath, "format", projectDir])
383+
);
384+
expect(formatCheckResult.exitCode).equals(0);
385+
386+
// Test with --check flag after formatting
387+
const afterFormatCheckResult = await getProcessResult(
388+
execFile(nodePath, [cliEntryPointPath, "format", projectDir, "--check"])
389+
);
390+
391+
// Should exit with code 0 when all files are properly formatted
392+
expect(afterFormatCheckResult.exitCode).equals(0);
393+
expect(afterFormatCheckResult.stdout).contains("All files are formatted correctly");
394+
});
307395
});

0 commit comments

Comments
 (0)