From a74b8e04ca56a5e31e854c7ebaedda5900db9de9 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 15 Jan 2024 10:47:53 +0100 Subject: [PATCH] feat: CLI command to run formatting (#824) Closes #702 ### Summary of Changes Add a new `format` command to format Safe-DS files. --- packages/safe-ds-cli/src/cli/format.ts | 35 ++++++++++++++ packages/safe-ds-cli/src/cli/main.ts | 8 ++++ .../safe-ds-cli/src/helpers/diagnostics.ts | 18 ++++++++ packages/safe-ds-cli/tests/cli/main.test.ts | 46 +++++++++++++++++++ .../format/contains syntax errors.sdstest | 3 ++ .../tests/resources/format/correct.sdstest | 3 ++ .../tests/resources/format/not safe-ds.txt | 3 ++ 7 files changed, 116 insertions(+) create mode 100644 packages/safe-ds-cli/src/cli/format.ts create mode 100644 packages/safe-ds-cli/tests/resources/format/contains syntax errors.sdstest create mode 100644 packages/safe-ds-cli/tests/resources/format/correct.sdstest create mode 100644 packages/safe-ds-cli/tests/resources/format/not safe-ds.txt diff --git a/packages/safe-ds-cli/src/cli/format.ts b/packages/safe-ds-cli/src/cli/format.ts new file mode 100644 index 000000000..f01ca003e --- /dev/null +++ b/packages/safe-ds-cli/src/cli/format.ts @@ -0,0 +1,35 @@ +import { createSafeDsServicesWithBuiltins } from '@safe-ds/lang'; +import { NodeFileSystem } from 'langium/node'; +import { extractDocuments } from '../helpers/documents.js'; +import { exitIfDocumentHasSyntaxErrors } from '../helpers/diagnostics.js'; +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { writeFile } from 'node:fs/promises'; +import chalk from 'chalk'; + +export const format = async (fsPaths: string[]): Promise => { + const services = (await createSafeDsServicesWithBuiltins(NodeFileSystem)).SafeDs; + const documents = await extractDocuments(services, fsPaths); + + // Exit if any document has syntax errors before formatting code + for (const document of documents) { + exitIfDocumentHasSyntaxErrors(document); + } + + // Format code + for (const document of documents) { + const edits = await services.lsp.Formatter!.formatDocument(document, { + textDocument: { + uri: document.uri.toString(), + }, + options: { + tabSize: 4, + insertSpaces: true, + }, + }); + + const editedDocument = TextDocument.applyEdits(document.textDocument, edits); + await writeFile(document.uri.fsPath, editedDocument); + } + + console.log(chalk.green(`Safe-DS code formatted successfully.`)); +}; diff --git a/packages/safe-ds-cli/src/cli/main.ts b/packages/safe-ds-cli/src/cli/main.ts index 53364324d..1a443f256 100644 --- a/packages/safe-ds-cli/src/cli/main.ts +++ b/packages/safe-ds-cli/src/cli/main.ts @@ -3,6 +3,7 @@ import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; import { generate } from './generate.js'; import { check } from './check.js'; +import { format } from './format.js'; const program = new Command(); @@ -19,6 +20,13 @@ program .description('check Safe-DS code') .action(check); +// Format command +program + .command('format') + .argument('', `list of files or directories to format`) + .description('format Safe-DS code') + .action(format); + // Generate command program .command('generate') diff --git a/packages/safe-ds-cli/src/helpers/diagnostics.ts b/packages/safe-ds-cli/src/helpers/diagnostics.ts index 92c367156..a6dbe6f64 100644 --- a/packages/safe-ds-cli/src/helpers/diagnostics.ts +++ b/packages/safe-ds-cli/src/helpers/diagnostics.ts @@ -99,6 +99,24 @@ export const exitIfDocumentHasErrors = function (document: LangiumDocument): voi } }; +/** + * Exits the process if the given document has syntax errors. + */ +export const exitIfDocumentHasSyntaxErrors = function (document: LangiumDocument): void { + const errors = getSyntaxErrors(document); + if (errors.length > 0) { + console.error(chalk.red(`The file '${uriToRelativePath(document.uri)}' has syntax errors:`)); + for (const error of errors) { + console.error(diagnosticToString(document.uri, error)); + } + process.exit(ExitCode.FileHasErrors); + } +}; + const getErrors = (document: LangiumDocument): Diagnostic[] => { return getDiagnostics(document).filter((it) => it.severity === DiagnosticSeverity.Error); }; + +const getSyntaxErrors = (document: LangiumDocument): Diagnostic[] => { + return getErrors(document).filter((d) => d.data?.code === 'lexing-error' || d.data?.code === 'parsing-error'); +}; diff --git a/packages/safe-ds-cli/tests/cli/main.test.ts b/packages/safe-ds-cli/tests/cli/main.test.ts index 691803c1c..2e8832c50 100644 --- a/packages/safe-ds-cli/tests/cli/main.test.ts +++ b/packages/safe-ds-cli/tests/cli/main.test.ts @@ -93,6 +93,52 @@ describe('safe-ds', () => { }); }); + describe('format', () => { + const testResourcesRoot = new URL('../resources/format/', import.meta.url); + const spawnFormatProcess = (additionalArguments: string[], paths: string[]) => { + const fsPaths = paths.map((p) => fileURLToPath(new URL(p, testResourcesRoot))); + return spawnSync('node', ['./bin/cli', 'format', ...additionalArguments, ...fsPaths], { + cwd: projectRoot, + }); + }; + + it('should show an error if no paths are passed', () => { + const process = spawnFormatProcess([], []); + expect(process.stderr.toString()).toContain("error: missing required argument 'paths'"); + expect(process.status).not.toBe(ExitCode.Success); + }); + + it('should show usage on stdout if -h flag is passed', () => { + const process = spawnFormatProcess(['-h'], []); + expect(process.stdout.toString()).toContain('Usage: cli format'); + expect(process.status).toBe(ExitCode.Success); + }); + + it('should show errors in wrong files', () => { + const process = spawnFormatProcess([], ['.']); + expect(process.stderr.toString()).toContain('has syntax errors'); + expect(process.status).toBe(ExitCode.FileHasErrors); + }); + + it('should show not show errors in correct files', () => { + const process = spawnFormatProcess([], ['correct.sdstest']); + expect(process.stdout.toString()).toContain('Safe-DS code formatted successfully.'); + expect(process.status).toBe(ExitCode.Success); + }); + + it('should show an error if the file does not exist', () => { + const process = spawnFormatProcess([], ['missing.sdstest']); + expect(process.stderr.toString()).toMatch(/Path .* does not exist\./u); + expect(process.status).toBe(ExitCode.MissingPath); + }); + + it('should show an error if a file has the wrong extension', () => { + const process = spawnFormatProcess([], ['not safe-ds.txt']); + expect(process.stderr.toString()).toContain('does not have a Safe-DS extension'); + expect(process.status).toBe(ExitCode.FileWithoutSafeDsExtension); + }); + }); + describe('generate', () => { const testResourcesRoot = new URL('../resources/generate/', import.meta.url); const spawnGenerateProcess = (additionalArguments: string[], paths: string[]) => { diff --git a/packages/safe-ds-cli/tests/resources/format/contains syntax errors.sdstest b/packages/safe-ds-cli/tests/resources/format/contains syntax errors.sdstest new file mode 100644 index 000000000..a8105c6c4 --- /dev/null +++ b/packages/safe-ds-cli/tests/resources/format/contains syntax errors.sdstest @@ -0,0 +1,3 @@ +package test + +segment mySegment() diff --git a/packages/safe-ds-cli/tests/resources/format/correct.sdstest b/packages/safe-ds-cli/tests/resources/format/correct.sdstest new file mode 100644 index 000000000..3e7427394 --- /dev/null +++ b/packages/safe-ds-cli/tests/resources/format/correct.sdstest @@ -0,0 +1,3 @@ +package test + +pipeline myPipeline {} diff --git a/packages/safe-ds-cli/tests/resources/format/not safe-ds.txt b/packages/safe-ds-cli/tests/resources/format/not safe-ds.txt new file mode 100644 index 000000000..3479b472c --- /dev/null +++ b/packages/safe-ds-cli/tests/resources/format/not safe-ds.txt @@ -0,0 +1,3 @@ +package test + +segment mySegment() {}