Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: CLI command to run validation #820

Merged
merged 13 commits into from
Jan 8, 2024
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