Skip to content

Commit

Permalink
feat: CLI command to run validation (#820)
Browse files Browse the repository at this point in the history
Closes #703

### Summary of Changes

Add a new `check` command to validate Safe-DS files without generating
code for them.
  • Loading branch information
lars-reimann authored Jan 8, 2024
1 parent d8fdde6 commit 7c2526d
Show file tree
Hide file tree
Showing 72 changed files with 781 additions and 158 deletions.
17 changes: 11 additions & 6 deletions package-lock.json

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

9 changes: 5 additions & 4 deletions packages/safe-ds-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
Usage: cli [options] [command]
Options:
-V, --version output the version number
-h, --help display help for command
-V, --version output the version number
-h, --help display help for command
Commands:
generate [options] <file> generate Python code
help [command] display help for command
check [options] <paths...> check Safe-DS code
generate [options] <paths...> generate Python code
help [command] display help for command
```
7 changes: 5 additions & 2 deletions packages/safe-ds-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,13 @@
"@safe-ds/lang": ">=0.3.0",
"chalk": "^5.3.0",
"commander": "^11.1.0",
"langium": "^2.1.3"
"glob": "^10.3.10",
"langium": "^2.1.3",
"true-myth": "^7.1.0"
},
"devDependencies": {
"@types/node": "^18.18.12"
"@types/node": "^18.18.12",
"vscode-languageserver": "^9.0.1"
},
"engines": {
"node": ">=18.0.0"
Expand Down
47 changes: 47 additions & 0 deletions packages/safe-ds-cli/src/cli/check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { createSafeDsServicesWithBuiltins } from '@safe-ds/lang';
import { NodeFileSystem } from 'langium/node';
import { extractDocuments } from '../helpers/documents.js';
import { diagnosticToString, getDiagnostics } from '../helpers/diagnostics.js';
import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver';
import chalk from 'chalk';
import { ExitCode } from './exitCode.js';

export const check = async (fsPaths: string[], options: CheckOptions): Promise<void> => {
const services = (await createSafeDsServicesWithBuiltins(NodeFileSystem)).SafeDs;

let errorCount = 0;

for (const document of await extractDocuments(services, fsPaths)) {
for (const diagnostic of getDiagnostics(document)) {
console.log(diagnosticToString(document.uri, diagnostic, options));

if (isError(diagnostic, options)) {
errorCount++;
}
}
}

if (errorCount > 0) {
console.error(chalk.red(`Found ${errorCount} ${errorCount === 1 ? 'error' : 'errors'}.`));
process.exit(ExitCode.FileHasErrors);
} else {
console.log(chalk.green(`No errors found.`));
}
};

/**
* Command line options for the `check` command.
*/
export interface CheckOptions {
/**
* Whether the program should fail on warnings.
*/
strict: boolean;
}

const isError = (diagnostic: Diagnostic, options: CheckOptions) => {
return (
diagnostic.severity === DiagnosticSeverity.Error ||
(diagnostic.severity === DiagnosticSeverity.Warning && options.strict)
);
};
37 changes: 0 additions & 37 deletions packages/safe-ds-cli/src/cli/cli-util.ts

This file was deleted.

29 changes: 29 additions & 0 deletions packages/safe-ds-cli/src/cli/exitCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Exit codes for the CLI.
*/
export enum ExitCode {
/**
* Everything went well.
*/
Success = 0,

/**
* The given path does not exist.
*/
MissingPath = 100,

/**
* The given path is not a file or directory.
*/
NotAFileOrDirectory = 101,

/**
* The given file does not have a Safe-DS extension.
*/
FileWithoutSafeDsExtension = 102,

/**
* The given file has errors.
*/
FileHasErrors = 103,
}
44 changes: 26 additions & 18 deletions packages/safe-ds-cli/src/cli/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,40 @@ import { URI } from 'langium';
import { NodeFileSystem } from 'langium/node';
import fs from 'node:fs';
import path from 'node:path';
import { extractDocument } from './cli-util.js';
import { extractDocuments } from '../helpers/documents.js';
import { makeParentDirectoriesSync } from '../helpers/files.js';
import { exitIfDocumentHasErrors } from '../helpers/diagnostics.js';

export const generate = async (fileName: string, opts: CliGenerateOptions): Promise<void> => {
export const generate = async (fsPaths: string[], options: GenerateOptions): Promise<void> => {
const services = (await createSafeDsServicesWithBuiltins(NodeFileSystem)).SafeDs;
const document = await extractDocument(fileName, services);
const destination = opts.destination ?? path.join(path.dirname(fileName), 'generated');
const generatedFiles = services.generation.PythonGenerator.generate(document, {
destination: URI.file(path.resolve(destination)),
createSourceMaps: opts.sourcemaps,
});
const documents = await extractDocuments(services, fsPaths);

for (const file of generatedFiles) {
const fsPath = URI.parse(file.uri).fsPath;
const parentDirectoryPath = path.dirname(fsPath);
if (!fs.existsSync(parentDirectoryPath)) {
fs.mkdirSync(parentDirectoryPath, { recursive: true });
}
// Exit if any document has errors before generating code
for (const document of documents) {
exitIfDocumentHasErrors(document);
}

fs.writeFileSync(fsPath, file.getText());
// Generate code
for (const document of documents) {
const generatedFiles = services.generation.PythonGenerator.generate(document, {
destination: URI.file(path.resolve(options.out)),
createSourceMaps: options.sourcemaps,
});

for (const file of generatedFiles) {
const fsPath = URI.parse(file.uri).fsPath;
makeParentDirectoriesSync(fsPath);
fs.writeFileSync(fsPath, file.getText());
}
}

console.log(chalk.green(`Python code generated successfully.`));
};

export interface CliGenerateOptions {
destination?: string;
/**
* Command line options for the `generate` command.
*/
export interface GenerateOptions {
out: string;
sourcemaps: boolean;
quiet: boolean;
}
22 changes: 12 additions & 10 deletions packages/safe-ds-cli/src/cli/main.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
import { SafeDsLanguageMetaData } from '@safe-ds/lang';
import { Command } from 'commander';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'url';
import { fileURLToPath } from 'node:url';
import { generate } from './generate.js';

const fileExtensions = SafeDsLanguageMetaData.fileExtensions.join(', ');
import { check } from './check.js';

const program = new Command();

// Version command
const packagePath = fileURLToPath(new URL('../../package.json', import.meta.url));
const require = createRequire(import.meta.url);
program.version(require(packagePath).version);

// Check command
program
// eslint-disable-next-line @typescript-eslint/no-var-requires
.version(require(packagePath).version);
.command('check')
.argument('<paths...>', `list of files or directories to check`)
.option('-s, --strict', 'whether the program should fail on warnings', false)
.description('check Safe-DS code')
.action(check);

// Generate command
program
.command('generate')
.argument('<file>', `possible file extensions: ${fileExtensions}`)
.option('-d, --destination <dir>', 'destination directory of generation')
.option('-r, --root <dir>', 'source root folder')
.option('-q, --quiet', 'whether the program should print something', false)
.argument('<paths...>', `list of files or directories to generate Python code for`)
.option('-o, --out <dir>', 'destination directory for generation', 'generated')
.option('-s, --sourcemaps', 'whether source maps should be generated', false)
.description('generate Python code')
.action(generate);
Expand Down
Loading

0 comments on commit 7c2526d

Please sign in to comment.