diff --git a/docs/development/validation-testing.md b/docs/development/validation-testing.md new file mode 100644 index 000000000..b37e9b7c1 --- /dev/null +++ b/docs/development/validation-testing.md @@ -0,0 +1,59 @@ +# Validation Testing + +Validation tests are data-driven instead of being specified explicitly. This document explains how to add a new +validation test. + +## Adding a validation test + +1. Create a new **folder** (not just a file!) in the `tests/resources/validation` directory or any subdirectory. Give + the folder a descriptive name, since the folder name becomes part of the test name. + + !!! tip "Skipping a test" + + If you want to skip a test, add the prefix `skip-` to the folder name. + +2. Add files with the extension `.sdstest`, `.sdspipe`, or `.sdsstub` **directly inside the folder**. All files in a + folder will be loaded into the same workspace, so they can reference each other. Files in different folders are + loaded into different workspaces, so they cannot reference each other. +3. Add the Safe-DS code that you want to test to the file. +4. Specify the expected validation results using test comments (see [below](#format-of-test-comments)) and test + markers (e.g. `fun »F«()`). The test comments are used to specify + * the presence or absence of an issue, + * the severity of the issue, and + * the message of the issue. + + The test markers are used to specify the location of the issue. Test comments and test markers are mapped to each + other by their position in the file, i.e. the first test comment corresponds to the first test marker, the second + test comment corresponds to the second test marker, etc. There may be more test comments than test markers, but not + the other way around. Any additional test comments are applied to the entire file. + +5. Run the tests. The test runner will automatically pick up the new test. + +## Format of test comments + +1. As usual, test comments are single-line comments that start with `$TEST$`. +2. Then, you specify whether the issue should be absent by writing `no` or present by writing nothing. +3. Next, you specify the severity of the issue by writing `error`, `warning`, `info`, or `hint`. +4. Finally, you can optionally specify the message of the issue enclosed in double-quotes. You can also add an `r` + before the opening double-quote to indicate that the expected message should be interpreted as a regular expression + that must match the entire actual message. + +Here are some examples: + +```ts +// $TEST$ error "Incompatible type." +``` + +We expect an error with the exact message `Incompatible type.`. + +```ts +// $TEST$ no warning "Name should be lowerCamelCase." +``` + +We expect no warning with the exact message `Name should be lowerCamelCase.`. + +```ts +// $TEST$ info r".*empty.*" +``` + +We expect an info with a message that matches the regular expression `.*empty.*`. diff --git a/mkdocs.yml b/mkdocs.yml index c06f46222..a0ba19986 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -39,6 +39,7 @@ nav: - Grammar Testing: development/grammar-testing.md - Scoping Testing: development/scoping-testing.md - Formatting Testing: development/formatting-testing.md + - Validation Testing: development/validation-testing.md - Langium Quickstart: development/langium-quickstart.md # Configuration of MkDocs & Material for MkDocs -------------------------------- diff --git a/src/language/constant/fileExtensions.ts b/src/language/constant/fileExtensions.ts index 407522a2c..0696f72c2 100644 --- a/src/language/constant/fileExtensions.ts +++ b/src/language/constant/fileExtensions.ts @@ -25,6 +25,11 @@ export const STUB_FILE_EXTENSION = 'sdsstub'; */ export const TEST_FILE_EXTENSION = 'sdstest'; +/** + * All file extensions that are supported by the Safe-DS language. + */ +export const SAFE_DS_FILE_EXTENSIONS = [PIPELINE_FILE_EXTENSION, STUB_FILE_EXTENSION, TEST_FILE_EXTENSION]; + /** * All file extensions that are supported by the Safe-DS language. */ diff --git a/src/language/validation/nameConvention.ts b/src/language/validation/nameConvention.ts new file mode 100644 index 000000000..348391ef9 --- /dev/null +++ b/src/language/validation/nameConvention.ts @@ -0,0 +1,124 @@ +import { SdsDeclaration } from '../generated/ast.js'; +import { ValidationAcceptor } from 'langium'; + +const blockLambdaPrefix = '__block_lambda_'; + +export const nameMustNotStartWithBlockLambdaPrefix = (node: SdsDeclaration, accept: ValidationAcceptor) => { + if (node.name.startsWith(blockLambdaPrefix)) { + accept( + 'error', + "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas.", + { + node, + property: 'name', + code: 'nameConvention/blockLambdaPrefix', + }, + ); + } +}; + +export const nameShouldHaveCorrectCasing = (node: SdsDeclaration, accept: ValidationAcceptor) => { + switch (node.$type) { + case 'SdsAnnotation': + if (!isUpperCamelCase(node.name)) { + acceptCasingWarning(node, 'annotations', 'UpperCamelCase', accept); + } + return; + case 'SdsAttribute': + if (!isLowerCamelCase(node.name)) { + acceptCasingWarning(node, 'attributes', 'lowerCamelCase', accept); + } + return; + case 'SdsBlockLambdaResult': + if (!isLowerCamelCase(node.name)) { + acceptCasingWarning(node, 'block lambda results', 'lowerCamelCase', accept); + } + return; + case 'SdsClass': + if (!isUpperCamelCase(node.name)) { + acceptCasingWarning(node, 'classes', 'UpperCamelCase', accept); + } + return; + case 'SdsEnum': + if (!isUpperCamelCase(node.name)) { + acceptCasingWarning(node, 'enums', 'UpperCamelCase', accept); + } + return; + case 'SdsEnumVariant': + if (!isUpperCamelCase(node.name)) { + acceptCasingWarning(node, 'enum variants', 'UpperCamelCase', accept); + } + return; + case 'SdsFunction': + if (!isLowerCamelCase(node.name)) { + acceptCasingWarning(node, 'functions', 'lowerCamelCase', accept); + } + return; + case 'SdsModule': + const segments = node.name.split('.'); + if (!segments.every(isLowerCamelCase)) { + accept('warning', 'All segments of the qualified name of a package should be lowerCamelCase.', { + node, + property: 'name', + code: 'nameConvention/casing', + }); + } + return; + case 'SdsParameter': + if (!isLowerCamelCase(node.name)) { + acceptCasingWarning(node, 'parameters', 'lowerCamelCase', accept); + } + return; + case 'SdsPipeline': + if (!isLowerCamelCase(node.name)) { + acceptCasingWarning(node, 'pipelines', 'lowerCamelCase', accept); + } + return; + case 'SdsPlaceholder': + if (!isLowerCamelCase(node.name)) { + acceptCasingWarning(node, 'placeholders', 'lowerCamelCase', accept); + } + return; + case 'SdsResult': + if (!isLowerCamelCase(node.name)) { + acceptCasingWarning(node, 'results', 'lowerCamelCase', accept); + } + return; + case 'SdsSchema': + if (!isUpperCamelCase(node.name)) { + acceptCasingWarning(node, 'schemas', 'UpperCamelCase', accept); + } + return; + case 'SdsSegment': + if (!isLowerCamelCase(node.name)) { + acceptCasingWarning(node, 'segments', 'lowerCamelCase', accept); + } + return; + case 'SdsTypeParameter': + if (!isUpperCamelCase(node.name)) { + acceptCasingWarning(node, 'type parameters', 'UpperCamelCase', accept); + } + return; + } +}; + +const isLowerCamelCase = (name: string): boolean => { + return /^[a-z][a-zA-Z0-9]*$/gu.test(name); +}; + +const isUpperCamelCase = (name: string): boolean => { + return /^[A-Z][a-zA-Z0-9]*$/gu.test(name); +}; + +const acceptCasingWarning = ( + node: SdsDeclaration, + nodeName: string, + expectedCasing: string, + accept: ValidationAcceptor, +) => { + accept('warning', `Names of ${nodeName} should be ${expectedCasing}.`, { + node, + property: 'name', + code: 'nameConvention/casing', + }); +}; diff --git a/src/language/validation/safe-ds-validator.ts b/src/language/validation/safe-ds-validator.ts index 406a089d5..62b641cae 100644 --- a/src/language/validation/safe-ds-validator.ts +++ b/src/language/validation/safe-ds-validator.ts @@ -1,6 +1,7 @@ import { ValidationChecks } from 'langium'; import { SafeDsAstType } from '../generated/ast.js'; import type { SafeDsServices } from '../safe-ds-module.js'; +import { nameMustNotStartWithBlockLambdaPrefix, nameShouldHaveCorrectCasing } from './nameConvention.js'; /** * Register custom validation checks. @@ -9,7 +10,7 @@ export const registerValidationChecks = function (services: SafeDsServices) { const registry = services.validation.ValidationRegistry; const validator = services.validation.SafeDsValidator; const checks: ValidationChecks = { - // Person: validator.checkPersonStartsWithCapital + SdsDeclaration: [nameMustNotStartWithBlockLambdaPrefix, nameShouldHaveCorrectCasing], }; registry.register(checks, validator); }; @@ -17,13 +18,4 @@ export const registerValidationChecks = function (services: SafeDsServices) { /** * Implementation of custom validations. */ -export class SafeDsValidator { - // checkPersonStartsWithCapital(person: Person, accept: ValidationAcceptor): void { - // if (person.name) { - // const firstChar = person.name.substring(0, 1); - // if (firstChar.toUpperCase() !== firstChar) { - // accept('warning', 'Person name should start with a capital.', { node: person, property: 'name' }); - // } - // } - // } -} +export class SafeDsValidator {} diff --git a/tests/helpers/diagnostics.ts b/tests/helpers/diagnostics.ts new file mode 100644 index 000000000..d36d10c46 --- /dev/null +++ b/tests/helpers/diagnostics.ts @@ -0,0 +1,30 @@ +import { validationHelper } from 'langium/test'; +import { LangiumServices } from 'langium'; +import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-types'; + +/** + * Get syntax errors from a code snippet. + * + * @param services The language services. + * @param code The code snippet to check. + * @returns The syntax errors. + */ +export const getSyntaxErrors = async (services: LangiumServices, code: string): Promise => { + const validationResult = await validationHelper(services)(code); + return validationResult.diagnostics.filter( + (d) => + d.severity === DiagnosticSeverity.Error && + (d.data?.code === 'lexing-error' || d.data?.code === 'parsing-error'), + ); +}; + +/** + * The code contains syntax errors. + */ +export class SyntaxErrorsInCodeError extends Error { + constructor(readonly syntaxErrors: Diagnostic[]) { + const syntaxErrorsAsString = syntaxErrors.map((e) => `- ${e.message}`).join(`\n`); + + super(`Code has syntax errors:\n${syntaxErrorsAsString}`); + } +} diff --git a/tests/helpers/testResources.test.ts b/tests/helpers/testResources.test.ts index ce62560e0..89bd4c004 100644 --- a/tests/helpers/testResources.test.ts +++ b/tests/helpers/testResources.test.ts @@ -1,11 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { listTestResources } from './testResources.js'; +import { listTestResources, listTestsResourcesGroupedByParentDirectory } from './testResources.js'; describe('listTestResources', () => { it('should yield all Safe-DS files in a directory that are not skipped', () => { - const result = listTestResources('helpers/listTestResources') - .map((path) => path.replace(/\\/gu, '/')) - .sort(); + const result = listTestResources('helpers/listTestResources'); const expected = [ 'pipeline file.sdspipe', 'stub file.sdsstub', @@ -13,7 +11,46 @@ describe('listTestResources', () => { 'nested/pipeline file.sdspipe', 'nested/stub file.sdsstub', 'nested/test file.sdstest', - ].sort(); - expect(result).toStrictEqual(expected); + ]; + expect(normalizePaths(result)).toStrictEqual(normalizePaths(expected)); }); }); + +describe('listTestResourcesGroupedByParentDirectory', () => { + it('should yield all Safe-DS files in a directory that are not skipped and group them by parent directory', () => { + const result = listTestsResourcesGroupedByParentDirectory('helpers/listTestResources'); + + const keys = Object.keys(result); + expect(normalizePaths(keys)).toStrictEqual(normalizePaths(['.', 'nested'])); + + const directlyInRoot = result['.']; + expect(normalizePaths(directlyInRoot)).toStrictEqual( + normalizePaths(['pipeline file.sdspipe', 'stub file.sdsstub', 'test file.sdstest']), + ); + + const inNested = result.nested; + expect(normalizePaths(inNested)).toStrictEqual( + normalizePaths(['nested/pipeline file.sdspipe', 'nested/stub file.sdsstub', 'nested/test file.sdstest']), + ); + }); +}); + +/** + * Normalizes the given paths by replacing backslashes with slashes and sorting them. + * + * @param paths The paths to normalize. + * @return The normalized paths. + */ +const normalizePaths = (paths: string[]): string[] => { + return paths.map(normalizePath).sort(); +}; + +/** + * Normalizes the given path by replacing backslashes with slashes. + * + * @param path The path to normalize. + * @return The normalized path. + */ +const normalizePath = (path: string): string => { + return path.replace(/\\/gu, '/'); +}; diff --git a/tests/helpers/testResources.ts b/tests/helpers/testResources.ts index 438fbca02..8a1435cda 100644 --- a/tests/helpers/testResources.ts +++ b/tests/helpers/testResources.ts @@ -1,10 +1,7 @@ import path from 'path'; import { globSync } from 'glob'; -import { - PIPELINE_FILE_EXTENSION, - STUB_FILE_EXTENSION, - TEST_FILE_EXTENSION, -} from '../../src/language/constant/fileExtensions.js'; +import { SAFE_DS_FILE_EXTENSIONS } from '../../src/language/constant/fileExtensions.js'; +import { group } from 'radash'; const resourcesPath = path.join(__dirname, '..', 'resources'); @@ -19,20 +16,32 @@ export const resolvePathRelativeToResources = (pathRelativeToResources: string) }; /** - * Lists all Safe-DS files in the given directory relative to `tests/resources/` except those that have a name starting - * with 'skip'. + * Lists all Safe-DS files in the given directory relative to `tests/resources/` that are not skipped. * * @param pathRelativeToResources The root directory relative to `tests/resources/`. * @return Paths to the Safe-DS files relative to `pathRelativeToResources`. */ export const listTestResources = (pathRelativeToResources: string): string[] => { - const fileExtensions = [PIPELINE_FILE_EXTENSION, STUB_FILE_EXTENSION, TEST_FILE_EXTENSION]; - const pattern = `**/*.{${fileExtensions.join(',')}}`; + const pattern = `**/*.{${SAFE_DS_FILE_EXTENSIONS.join(',')}}`; const cwd = resolvePathRelativeToResources(pathRelativeToResources); return globSync(pattern, { cwd, nodir: true }).filter(isNotSkipped); }; +/** + * Lists all Safe-DS files in the given directory relative to `tests/resources/` that are not skipped. The result is + * grouped by the parent directory. + * + * @param pathRelativeToResources The root directory relative to `tests/resources/`. + * @return Paths to the Safe-DS files relative to `pathRelativeToResources` grouped by the parent directory. + */ +export const listTestsResourcesGroupedByParentDirectory = ( + pathRelativeToResources: string, +): Record => { + const paths = listTestResources(pathRelativeToResources); + return group(paths, (p) => path.dirname(p)) as Record; +}; + const isNotSkipped = (pathRelativeToResources: string) => { const segments = pathRelativeToResources.split(path.sep); return !segments.some((segment) => segment.startsWith('skip')); diff --git a/tests/language/formatting/creator.ts b/tests/language/formatting/creator.ts index 8c1280696..59b051985 100644 --- a/tests/language/formatting/creator.ts +++ b/tests/language/formatting/creator.ts @@ -1,46 +1,53 @@ -import { listTestResources, resolvePathRelativeToResources } from '../../helpers/testResources'; +import { listTestResources, resolvePathRelativeToResources } from '../../helpers/testResources.js'; import path from 'path'; import fs from 'fs'; -import { validationHelper } from 'langium/test'; import { Diagnostic } from 'vscode-languageserver-types'; -import { createSafeDsServices } from '../../../src/language/safe-ds-module'; +import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; import { EmptyFileSystem } from 'langium'; +import { getSyntaxErrors } from '../../helpers/diagnostics.js'; const services = createSafeDsServices(EmptyFileSystem).SafeDs; +const root = 'formatting'; const separator = '// -----------------------------------------------------------------------------'; -export const createFormatterTests = async (): Promise => { - const testCases = listTestResources('formatting').map(async (pathRelativeToResources): Promise => { - const absolutePath = resolvePathRelativeToResources(path.join('formatting', pathRelativeToResources)); - const program = fs.readFileSync(absolutePath).toString(); - const parts = program.split(separator); +export const createFormattingTests = async (): Promise => { + const testCases = listTestResources(root).map(createFormattingTest); + return Promise.all(testCases); +}; - // Must contain exactly one separator - if (parts.length !== 2) { - return invalidTest(pathRelativeToResources, new SeparatorError(parts.length - 1)); - } +const createFormattingTest = async (relativeResourcePath: string): Promise => { + const absolutePath = resolvePathRelativeToResources(path.join(root, relativeResourcePath)); + const program = fs.readFileSync(absolutePath).toString(); + const parts = program.split(separator); - // Original code must not contain syntax errors - const originalCode = normalizeLineBreaks(parts[0]).trimEnd(); - const expectedFormattedCode = normalizeLineBreaks(parts[1]).trim(); + // Must contain exactly one separator + if (parts.length !== 2) { + return invalidTest(relativeResourcePath, new SeparatorError(parts.length - 1)); + } - const validationResult = await validationHelper(services)(parts[0]); - const syntaxErrors = validationResult.diagnostics.filter( - (d) => d.severity === 1 && (d.code === 'lexing-error' || d.code === 'parsing-error'), - ); + const originalCode = normalizeLineBreaks(parts[0]).trimEnd(); + const expectedFormattedCode = normalizeLineBreaks(parts[1]).trim(); - if (syntaxErrors.length > 0) { - return invalidTest(pathRelativeToResources, new SyntaxErrorsInOriginalCodeError(syntaxErrors)); - } + // Original code must not contain syntax errors + const syntaxErrorsInOriginalCode = await getSyntaxErrors(services, originalCode); + if (syntaxErrorsInOriginalCode.length > 0) { + return invalidTest(relativeResourcePath, new SyntaxErrorsInOriginalCodeError(syntaxErrorsInOriginalCode)); + } - return { - testName: `${pathRelativeToResources} should be formatted correctly`, - originalCode, - expectedFormattedCode, - }; - }); + // Expected formatted code must not contain syntax errors + const syntaxErrorsInExpectedFormattedCode = await getSyntaxErrors(services, expectedFormattedCode); + if (syntaxErrorsInExpectedFormattedCode.length > 0) { + return invalidTest( + relativeResourcePath, + new SyntaxErrorsInExpectedFormattedCodeError(syntaxErrorsInExpectedFormattedCode), + ); + } - return Promise.all(testCases); + return { + testName: `${relativeResourcePath} should be formatted correctly`, + originalCode, + expectedFormattedCode, + }; }; /** @@ -49,7 +56,7 @@ export const createFormatterTests = async (): Promise => { * @param pathRelativeToResources The path to the test file relative to the resources directory. * @param error The error that occurred. */ -const invalidTest = (pathRelativeToResources: string, error: Error): FormatterTest => { +const invalidTest = (pathRelativeToResources: string, error: Error): FormattingTest => { return { testName: `INVALID TEST FILE [${pathRelativeToResources}]`, originalCode: '', @@ -69,9 +76,9 @@ const normalizeLineBreaks = (code: string): string => { }; /** - * A description of a formatter test. + * A description of a formatting test. */ -interface FormatterTest { +interface FormattingTest { /** * The name of the test. */ @@ -94,7 +101,7 @@ interface FormatterTest { } /** - * The file contained no or more than one separator. + * The file contains no or more than one separator. */ class SeparatorError extends Error { constructor(readonly number_of_separators: number) { @@ -103,7 +110,7 @@ class SeparatorError extends Error { } /** - * The original code contained syntax errors. + * The original code contains syntax errors. */ class SyntaxErrorsInOriginalCodeError extends Error { constructor(readonly syntaxErrors: Diagnostic[]) { @@ -112,3 +119,14 @@ class SyntaxErrorsInOriginalCodeError extends Error { super(`Original code has syntax errors:\n${syntaxErrorsAsString}`); } } + +/** + * The expected formatted code contains syntax errors. + */ +class SyntaxErrorsInExpectedFormattedCodeError extends Error { + constructor(readonly syntaxErrors: Diagnostic[]) { + const syntaxErrorsAsString = syntaxErrors.map((e) => `- ${e.message}`).join(`\n`); + + super(`Expected formatted code has syntax errors:\n${syntaxErrorsAsString}`); + } +} diff --git a/tests/language/formatting/testFormatter.test.ts b/tests/language/formatting/testFormatting.test.ts similarity index 92% rename from tests/language/formatting/testFormatter.test.ts rename to tests/language/formatting/testFormatting.test.ts index 62cf1c3bc..3b9f55f3d 100644 --- a/tests/language/formatting/testFormatter.test.ts +++ b/tests/language/formatting/testFormatting.test.ts @@ -1,11 +1,11 @@ -import { createSafeDsServices } from '../../../src/language/safe-ds-module'; +import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; import { clearDocuments, expectFormatting } from 'langium/test'; import { describe, it } from 'vitest'; import { EmptyFileSystem } from 'langium'; -import { createFormatterTests } from './creator'; +import { createFormattingTests } from './creator.js'; const services = createSafeDsServices(EmptyFileSystem).SafeDs; -const formatterTests = createFormatterTests(); +const formatterTests = createFormattingTests(); describe('formatter', async () => { // Test that the original code is formatted correctly diff --git a/tests/language/grammar/creator.ts b/tests/language/grammar/creator.ts index ac6bb7c8e..8941b8ea5 100644 --- a/tests/language/grammar/creator.ts +++ b/tests/language/grammar/creator.ts @@ -1,45 +1,49 @@ -import { listTestResources, resolvePathRelativeToResources } from '../../helpers/testResources'; +import { listTestResources, resolvePathRelativeToResources } from '../../helpers/testResources.js'; import path from 'path'; import fs from 'fs'; -import { findTestComments } from '../../helpers/testComments'; -import { NoCommentsError } from '../../helpers/testChecks'; +import { findTestComments } from '../../helpers/testComments.js'; +import { NoCommentsError } from '../../helpers/testChecks.js'; + +const root = 'grammar'; export const createGrammarTests = (): GrammarTest[] => { - return listTestResources('grammar').map((pathRelativeToResources): GrammarTest => { - const absolutePath = resolvePathRelativeToResources(path.join('grammar', pathRelativeToResources)); - const code = fs.readFileSync(absolutePath).toString(); - const comments = findTestComments(code); - - // Must contain at least one comment - if (comments.length === 0) { - return invalidTest(pathRelativeToResources, new NoCommentsError()); - } - - // Must contain no more than one comment - if (comments.length > 1) { - return invalidTest(pathRelativeToResources, new MultipleCommentsError(comments)); - } - - const comment = comments[0]; - - // Must contain a valid comment - if (comment !== 'syntax_error' && comment !== 'no_syntax_error') { - return invalidTest(pathRelativeToResources, new InvalidCommentError(comment)); - } - - let testName: string; - if (comment === 'syntax_error') { - testName = `[${pathRelativeToResources}] should have syntax errors`; - } else { - testName = `[${pathRelativeToResources}] should not have syntax errors`; - } - - return { - testName, - code, - expectedResult: comment, - }; - }); + return listTestResources(root).map(createGrammarTest); +}; + +const createGrammarTest = (relativeResourcePath: string): GrammarTest => { + const absolutePath = resolvePathRelativeToResources(path.join(root, relativeResourcePath)); + const code = fs.readFileSync(absolutePath).toString(); + const comments = findTestComments(code); + + // Must contain at least one comment + if (comments.length === 0) { + return invalidTest(relativeResourcePath, new NoCommentsError()); + } + + // Must contain no more than one comment + if (comments.length > 1) { + return invalidTest(relativeResourcePath, new MultipleCommentsError(comments)); + } + + const comment = comments[0]; + + // Must contain a valid comment + if (comment !== 'syntax_error' && comment !== 'no_syntax_error') { + return invalidTest(relativeResourcePath, new InvalidCommentError(comment)); + } + + let testName: string; + if (comment === 'syntax_error') { + testName = `[${relativeResourcePath}] should have syntax errors`; + } else { + testName = `[${relativeResourcePath}] should not have syntax errors`; + } + + return { + testName, + code, + expectedResult: comment, + }; }; /** diff --git a/tests/language/grammar/testGrammar.test.ts b/tests/language/grammar/testGrammar.test.ts index b45112b81..80803cb6b 100644 --- a/tests/language/grammar/testGrammar.test.ts +++ b/tests/language/grammar/testGrammar.test.ts @@ -1,9 +1,10 @@ import { describe, it } from 'vitest'; -import { createSafeDsServices } from '../../../src/language/safe-ds-module'; +import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; import { AssertionError } from 'assert'; import { NodeFileSystem } from 'langium/node'; -import { createGrammarTests } from './creator'; -import { clearDocuments, validationHelper } from 'langium/test'; +import { createGrammarTests } from './creator.js'; +import { clearDocuments } from 'langium/test'; +import { getSyntaxErrors } from '../../helpers/diagnostics.js'; const services = createSafeDsServices(NodeFileSystem).SafeDs; @@ -15,17 +16,14 @@ describe('grammar', () => { } // Get the actual syntax errors - const { diagnostics } = await validationHelper(services)(test.code); - const syntaxErrors = diagnostics.filter( - (d) => d.severity === 1 && (d.data.code === 'lexing-error' || d.data.code === 'parsing-error'), - ); + const actualSyntaxErrors = await getSyntaxErrors(services, test.code); // Expected syntax errors if (test.expectedResult === 'syntax_error') { - if (syntaxErrors.length === 0) { + if (actualSyntaxErrors.length === 0) { throw new AssertionError({ message: 'Expected syntax errors but found none.', - actual: syntaxErrors, + actual: actualSyntaxErrors, expected: [], }); } @@ -33,10 +31,10 @@ describe('grammar', () => { // Expected no syntax errors else if (test.expectedResult === 'no_syntax_error') { - if (syntaxErrors.length > 0) { + if (actualSyntaxErrors.length > 0) { throw new AssertionError({ message: 'Expected no syntax errors but found some.', - actual: syntaxErrors, + actual: actualSyntaxErrors, expected: [], }); } diff --git a/tests/language/scoping/creator.ts b/tests/language/scoping/creator.ts index 195f1d39c..12f6e2295 100644 --- a/tests/language/scoping/creator.ts +++ b/tests/language/scoping/creator.ts @@ -1,38 +1,57 @@ -import { listTestResources, resolvePathRelativeToResources } from '../../helpers/testResources'; -import { group } from 'radash'; +import { + listTestsResourcesGroupedByParentDirectory, + resolvePathRelativeToResources, +} from '../../helpers/testResources.js'; import path from 'path'; import fs from 'fs'; -import { findTestChecks } from '../../helpers/testChecks'; +import { findTestChecks } from '../../helpers/testChecks.js'; import { Location } from 'vscode-languageserver'; import { URI } from 'vscode-uri'; +import { getSyntaxErrors, SyntaxErrorsInCodeError } from '../../helpers/diagnostics.js'; +import { EmptyFileSystem } from 'langium'; +import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; -export const createScopingTests = (): ScopingTest[] => { - const pathsRelativeToResources = listTestResources('scoping'); - const pathsRelativeToResourcesGroupedByDirname = group(pathsRelativeToResources, (pathRelativeToResources) => - path.dirname(pathRelativeToResources), - ) as Record; +const services = createSafeDsServices(EmptyFileSystem).SafeDs; +const root = 'scoping'; - return Object.entries(pathsRelativeToResourcesGroupedByDirname).map(([dirname, paths]) => +export const createScopingTests = (): Promise => { + const pathsGroupedByParentDirectory = listTestsResourcesGroupedByParentDirectory(root); + const testCases = Object.entries(pathsGroupedByParentDirectory).map(([dirname, paths]) => createScopingTest(dirname, paths), ); + + return Promise.all(testCases); }; -const createScopingTest = (dirnameRelativeToResources: string, pathsRelativeToResources: string[]): ScopingTest => { +const createScopingTest = async ( + relativeParentDirectoryPath: string, + relativeResourcePaths: string[], +): Promise => { const uris: string[] = []; const references: ExpectedReferenceWithTargetId[] = []; const targets: Map = new Map(); - for (const pathRelativeToResources of pathsRelativeToResources) { - const absolutePath = resolvePathRelativeToResources(path.join('scoping', pathRelativeToResources)); + for (const relativeResourcePath of relativeResourcePaths) { + const absolutePath = resolvePathRelativeToResources(path.join(root, relativeResourcePath)); const uri = URI.file(absolutePath).toString(); uris.push(uri); const code = fs.readFileSync(absolutePath).toString(); + + // File must not contain any syntax errors + const syntaxErrors = await getSyntaxErrors(services, code); + if (syntaxErrors.length > 0) { + return invalidTest( + `INVALID TEST FILE [${relativeResourcePath}]`, + new SyntaxErrorsInCodeError(syntaxErrors), + ); + } + const checksResult = findTestChecks(code, uri, { failIfFewerRangesThanComments: true }); // Something went wrong when finding test checks if (checksResult.isErr) { - return invalidTest(`INVALID TEST FILE [${pathRelativeToResources}]`, checksResult.error); + return invalidTest(`INVALID TEST FILE [${relativeResourcePath}]`, checksResult.error); } for (const check of checksResult.value) { @@ -61,7 +80,7 @@ const createScopingTest = (dirnameRelativeToResources: string, pathsRelativeToRe if (targets.has(id)) { return invalidTest( - `INVALID TEST SUITE [${dirnameRelativeToResources}]`, + `INVALID TEST SUITE [${relativeParentDirectoryPath}]`, new DuplicateTargetIdError(id), ); } else { @@ -73,10 +92,7 @@ const createScopingTest = (dirnameRelativeToResources: string, pathsRelativeToRe continue; } - return invalidTest( - `INVALID TEST FILE [${pathRelativeToResources}]`, - new InvalidCommentError(check.comment), - ); + return invalidTest(`INVALID TEST FILE [${relativeResourcePath}]`, new InvalidCommentError(check.comment)); } } @@ -85,7 +101,7 @@ const createScopingTest = (dirnameRelativeToResources: string, pathsRelativeToRe if (reference.targetId) { if (!targets.has(reference.targetId)) { return invalidTest( - `INVALID TEST SUITE [${dirnameRelativeToResources}]`, + `INVALID TEST SUITE [${relativeParentDirectoryPath}]`, new MissingTargetError(reference.targetId), ); } @@ -95,7 +111,7 @@ const createScopingTest = (dirnameRelativeToResources: string, pathsRelativeToRe } return { - testName: `[${dirnameRelativeToResources}] should be scoped correctly`, + testName: `[${relativeParentDirectoryPath}] should be scoped correctly`, uris, expectedReferences: references, }; diff --git a/tests/language/scoping/testScoping.test.ts b/tests/language/scoping/testScoping.test.ts index 2f3cc0527..11aab4776 100644 --- a/tests/language/scoping/testScoping.test.ts +++ b/tests/language/scoping/testScoping.test.ts @@ -1,18 +1,18 @@ import { describe, it } from 'vitest'; -import { createSafeDsServices } from '../../../src/language/safe-ds-module'; +import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; import { URI } from 'vscode-uri'; import { NodeFileSystem } from 'langium/node'; import { isRangeEqual } from 'langium/test'; import { AssertionError } from 'assert'; -import { isLocationEqual, locationToString } from '../../helpers/location'; -import { createScopingTests, ExpectedReference } from './creator'; +import { isLocationEqual, locationToString } from '../../helpers/location.js'; +import { createScopingTests, ExpectedReference } from './creator.js'; import { LangiumDocument, Reference } from 'langium'; import { Location } from 'vscode-languageserver'; const services = createSafeDsServices(NodeFileSystem).SafeDs; -describe('scoping', () => { - it.each(createScopingTests())('$testName', async (test) => { +describe('scoping', async () => { + it.each(await createScopingTests())('$testName', async (test) => { // Test is invalid if (test.error) { throw test.error; diff --git a/tests/language/validation/creator.ts b/tests/language/validation/creator.ts new file mode 100644 index 000000000..0de3fd37c --- /dev/null +++ b/tests/language/validation/creator.ts @@ -0,0 +1,204 @@ +import { + listTestsResourcesGroupedByParentDirectory, + resolvePathRelativeToResources, +} from '../../helpers/testResources.js'; +import path from 'path'; +import fs from 'fs'; +import { findTestChecks } from '../../helpers/testChecks.js'; +import { URI } from 'vscode-uri'; +import { getSyntaxErrors, SyntaxErrorsInCodeError } from '../../helpers/diagnostics.js'; +import { EmptyFileSystem } from 'langium'; +import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; +import { DocumentUri, Range } from 'vscode-languageserver-types'; + +const services = createSafeDsServices(EmptyFileSystem).SafeDs; +const root = 'validation'; + +export const createValidationTests = (): Promise => { + const pathsGroupedByParentDirectory = listTestsResourcesGroupedByParentDirectory(root); + const testCases = Object.entries(pathsGroupedByParentDirectory).map(([dirname, paths]) => + createValidationTest(dirname, paths), + ); + + return Promise.all(testCases); +}; + +const createValidationTest = async ( + relativeParentDirectoryPath: string, + relativeResourcePaths: string[], +): Promise => { + const uris: string[] = []; + const issues: ExpectedIssue[] = []; + + for (const relativeResourcePath of relativeResourcePaths) { + const absolutePath = resolvePathRelativeToResources(path.join(root, relativeResourcePath)); + const uri = URI.file(absolutePath).toString(); + uris.push(uri); + + const code = fs.readFileSync(absolutePath).toString(); + + // File must not contain any syntax errors + const syntaxErrors = await getSyntaxErrors(services, code); + if (syntaxErrors.length > 0) { + return invalidTest( + `INVALID TEST FILE [${relativeResourcePath}]`, + new SyntaxErrorsInCodeError(syntaxErrors), + ); + } + + const checksResult = findTestChecks(code, uri); + + // Something went wrong when finding test checks + if (checksResult.isErr) { + return invalidTest(`INVALID TEST FILE [${relativeResourcePath}]`, checksResult.error); + } + + for (const check of checksResult.value) { + const regex = /\s*(?no\s+)?(?\S+)\s*(?:(?r)?"(?[^"]*)")?/gu; + const match = regex.exec(check.comment); + + // Overall comment is invalid + if (!match) { + return invalidTest( + `INVALID TEST FILE [${relativeResourcePath}]`, + new InvalidCommentError(check.comment), + ); + } + + // Extract groups from the match + const presence = match.groups!.isAbsent ? 'absent' : 'present'; + const severity = match.groups!.severity; + const messageIsRegex = match.groups!.messageIsRegex === 'r'; + const message = match.groups!.message; + + // Validate the severity + if (!validSeverities.includes(severity as any)) { + return invalidTest(`INVALID TEST FILE [${relativeResourcePath}]`, new InvalidSeverityError(severity)); + } + + // Add the issue + issues.push({ + presence, + severity: severity as Severity, + message, + messageIsRegex, + uri, + range: check.location?.range, + }); + } + } + + return { + testName: `[${relativeParentDirectoryPath}] should be validated correctly`, + uris, + expectedIssues: issues, + }; +}; + +/** + * Report a test that has errors. + * + * @param testName The name of the test. + * @param error The error that occurred. + */ +const invalidTest = (testName: string, error: Error): ValidationTest => { + return { + testName, + uris: [], + expectedIssues: [], + error, + }; +}; + +/** + * A description of a validation test. + */ +interface ValidationTest { + /** + * The name of the test. + */ + testName: string; + + /** + * The URIs of the files that should be loaded into the workspace. + */ + uris: string[]; + + /** + * The issues we expect to find in the workspace. + */ + expectedIssues: ExpectedIssue[]; + + /** + * An error that occurred while creating the test. If this is undefined, the test is valid. + */ + error?: Error; +} + +/** + * An issue that is expected to be present in or absent from the workspace. + */ +export interface ExpectedIssue { + /** + * Whether the issue should be present or absent. + */ + presence: Presence; + + /** + * The severity of the issue. + */ + severity: Severity; + + /** + * The message of the issue. + */ + message?: string; + + /** + * Whether the message should be interpreted as a regular expression. + */ + messageIsRegex?: boolean; + + /** + * The URI of the file containing the issue. + */ + uri: DocumentUri; + + /** + * The range of the issue. If undefined, the issue is expected to be present in the whole file. + */ + range?: Range; +} + +/** + * Whether the issue should be present or absent. + */ +export type Presence = 'present' | 'absent'; + +/** + * The valid severities of an issue. + */ +const validSeverities = ['error', 'warning', 'info', 'hint'] as const; + +/** + * The severity of the issue. + */ +export type Severity = (typeof validSeverities)[number]; + +/** + * A test comment did not match the expected format. + */ +class InvalidCommentError extends Error { + constructor(readonly comment: string) { + super(`Invalid test comment (refer to the documentation for guidance): ${comment}`); + } +} + +/** + * A test comment did not specify a valid severity. + */ +class InvalidSeverityError extends Error { + constructor(readonly type: string) { + super(`Invalid severity (valid values are ${validSeverities.join(', ')}): ${type}`); + } +} diff --git a/tests/language/validation/testValidation.test.ts b/tests/language/validation/testValidation.test.ts new file mode 100644 index 000000000..98f21357e --- /dev/null +++ b/tests/language/validation/testValidation.test.ts @@ -0,0 +1,113 @@ +import { describe, it } from 'vitest'; +import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; +import { URI } from 'vscode-uri'; +import { NodeFileSystem } from 'langium/node'; +import { createValidationTests, ExpectedIssue } from './creator.js'; +import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-types'; +import { AssertionError } from 'assert'; +import { isRangeEqual } from 'langium/test'; +import { locationToString } from '../../helpers/location.js'; + +const services = createSafeDsServices(NodeFileSystem).SafeDs; + +describe('validation', async () => { + it.each(await createValidationTests())('$testName', async (test) => { + // Test is invalid + if (test.error) { + throw test.error; + } + + // Load all documents + const documents = test.uris.map((uri) => + services.shared.workspace.LangiumDocuments.getOrCreateDocument(URI.parse(uri)), + ); + await services.shared.workspace.DocumentBuilder.build(documents, { validation: true }); + + // Ensure all expected issues match + for (const expectedIssue of test.expectedIssues) { + const actualIssues = getMatchingActualIssues(expectedIssue); + + // Expected to find a matching issue + if (expectedIssue.presence === 'present') { + if (actualIssues.length === 0) { + throw new AssertionError({ + message: `Expected to find a matching issue ${issueLocationToString( + expectedIssue, + )} but found none.`, + actual: [], + expected: [expectedIssue], + }); + } + } + + // Expected to find no matching issue + else { + if (actualIssues.length > 0) { + throw new AssertionError({ + message: `Expected to find no matching issue ${issueLocationToString( + expectedIssue, + )} but found some.`, + actual: actualIssues, + expected: [], + }); + } + } + } + }); +}); + +/** + * Find the actual issues matching the expected issues. + * + * @param expectedIssue The expected issue. + */ +const getMatchingActualIssues = (expectedIssue: ExpectedIssue): Diagnostic[] => { + const document = services.shared.workspace.LangiumDocuments.getOrCreateDocument(URI.parse(expectedIssue.uri)); + let result = document.diagnostics ?? []; + + // Filter by severity + switch (expectedIssue.severity) { + case 'error': + result = result.filter((d) => d.severity === DiagnosticSeverity.Error); + break; + case 'warning': + result = result.filter((d) => d.severity === DiagnosticSeverity.Warning); + break; + case 'info': + result = result.filter((d) => d.severity === DiagnosticSeverity.Information); + break; + case 'hint': + result = result.filter((d) => d.severity === DiagnosticSeverity.Hint); + break; + } + + // Filter by message + if (expectedIssue.message) { + if (expectedIssue.messageIsRegex) { + const regex = new RegExp(expectedIssue.message, 'gu'); + result = result.filter((d) => regex.test(d.message)); + } else { + result = result.filter((d) => d.message === expectedIssue.message); + } + } + + // Filter by range + if (expectedIssue.range) { + result = result.filter((d) => isRangeEqual(d.range, expectedIssue.range!)); + } + + return result; +}; + +/** + * Converts the location of an expected issue to a string. + * + * @param expectedIssue The issue. + */ +const issueLocationToString = (expectedIssue: ExpectedIssue): string => { + if (expectedIssue.range) { + return `at ${locationToString({ uri: expectedIssue.uri, range: expectedIssue.range })}`; + } else { + return `in ${expectedIssue.uri}`; + } +}; diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/annotations.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/annotations.sdstest new file mode 100644 index 000000000..f7e1c3911 --- /dev/null +++ b/tests/resources/validation/name convention/__block_lambda_ prefix/annotations.sdstest @@ -0,0 +1,7 @@ +package tests.validation.nameConvention.blockLambdaPrefix + +// $TEST$ error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." +annotation »__block_lambda_0« + +// $TEST$ no error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." +annotation »_block_lambda_1« diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/attributes.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/attributes.sdstest new file mode 100644 index 000000000..bf102c877 --- /dev/null +++ b/tests/resources/validation/name convention/__block_lambda_ prefix/attributes.sdstest @@ -0,0 +1,9 @@ +package tests.validation.nameConvention.blockLambdaPrefix + +class MyClass { + // $TEST$ error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." + attr »__block_lambda_0«: Int + + // $TEST$ no error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." + attr »_block_lambda_1«: Int +} diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/block lambda results.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/block lambda results.sdstest new file mode 100644 index 000000000..63e281ded --- /dev/null +++ b/tests/resources/validation/name convention/__block_lambda_ prefix/block lambda results.sdstest @@ -0,0 +1,11 @@ +package tests.validation.nameConvention.blockLambdaPrefix + +pipeline myPipeline2 { + () { + // $TEST$ error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." + yield »__block_lambda_0« = 1; + + // $TEST$ no error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." + yield »_block_lambda_1« = 1; + }; +} diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/classes.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/classes.sdstest new file mode 100644 index 000000000..faf15828d --- /dev/null +++ b/tests/resources/validation/name convention/__block_lambda_ prefix/classes.sdstest @@ -0,0 +1,7 @@ +package tests.validation.nameConvention.blockLambdaPrefix + +// $TEST$ error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." +class »__block_lambda_0« + +// $TEST$ no error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." +class »_block_lambda_1« diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/enum variants.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/enum variants.sdstest new file mode 100644 index 000000000..463705f84 --- /dev/null +++ b/tests/resources/validation/name convention/__block_lambda_ prefix/enum variants.sdstest @@ -0,0 +1,9 @@ +package tests.validation.nameConvention.blockLambdaPrefix + +enum MyEnum { + // $TEST$ error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." + »__block_lambda_0« + + // $TEST$ no error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." + »_block_lambda_1« +} diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/enums.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/enums.sdstest new file mode 100644 index 000000000..3b28ee32b --- /dev/null +++ b/tests/resources/validation/name convention/__block_lambda_ prefix/enums.sdstest @@ -0,0 +1,7 @@ +package tests.validation.nameConvention.blockLambdaPrefix + +// $TEST$ error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." +enum »__block_lambda_0« + +// $TEST$ no error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." +enum »_block_lambda_1« diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/functions.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/functions.sdstest new file mode 100644 index 000000000..cc1a0d55f --- /dev/null +++ b/tests/resources/validation/name convention/__block_lambda_ prefix/functions.sdstest @@ -0,0 +1,7 @@ +package tests.validation.nameConvention.blockLambdaPrefix + +// $TEST$ error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." +fun »__block_lambda_0«() + +// $TEST$ no error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." +fun »_block_lambda_1«() diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/packages with block lambda prefix.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/packages with block lambda prefix.sdstest new file mode 100644 index 000000000..4c00c1f81 --- /dev/null +++ b/tests/resources/validation/name convention/__block_lambda_ prefix/packages with block lambda prefix.sdstest @@ -0,0 +1,2 @@ +// $TEST$ error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." +package »__block_lambda_0« diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/packages without block lambda prefix.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/packages without block lambda prefix.sdstest new file mode 100644 index 000000000..d09e0f930 --- /dev/null +++ b/tests/resources/validation/name convention/__block_lambda_ prefix/packages without block lambda prefix.sdstest @@ -0,0 +1,2 @@ +// $TEST$ no error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." +package »_block_lambda_1« diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/parameters.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/parameters.sdstest new file mode 100644 index 000000000..cce1fb7ef --- /dev/null +++ b/tests/resources/validation/name convention/__block_lambda_ prefix/parameters.sdstest @@ -0,0 +1,9 @@ +package tests.validation.nameConvention.blockLambdaPrefix + +fun myFunction1( + // $TEST$ error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." + »__block_lambda_0«: Int, + + // $TEST$ no error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." + »_block_lambda_1«: Int, +) diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/pipelines.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/pipelines.sdstest new file mode 100644 index 000000000..bfffa3d31 --- /dev/null +++ b/tests/resources/validation/name convention/__block_lambda_ prefix/pipelines.sdstest @@ -0,0 +1,7 @@ +package tests.validation.nameConvention.blockLambdaPrefix + +// $TEST$ error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." +pipeline »__block_lambda_0« {} + +// $TEST$ no error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." +pipeline »_block_lambda_1« {} diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/placeholders.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/placeholders.sdstest new file mode 100644 index 000000000..819ecf56b --- /dev/null +++ b/tests/resources/validation/name convention/__block_lambda_ prefix/placeholders.sdstest @@ -0,0 +1,9 @@ +package tests.validation.nameConvention.blockLambdaPrefix + +pipeline myPipeline1 { + // $TEST$ error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." + val »__block_lambda_0« = 1; + + // $TEST$ no error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." + val »_block_lambda_1« = 1; +} diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/results.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/results.sdstest new file mode 100644 index 000000000..e0d7e9dff --- /dev/null +++ b/tests/resources/validation/name convention/__block_lambda_ prefix/results.sdstest @@ -0,0 +1,9 @@ +package tests.validation.nameConvention.blockLambdaPrefix + +fun myFunction2() -> ( + // $TEST$ error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." + »__block_lambda_0«: Int, + + // $TEST$ no error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." + »_block_lambda_1«: Int, +) diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/schemas.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/schemas.sdstest new file mode 100644 index 000000000..7d7b68d6f --- /dev/null +++ b/tests/resources/validation/name convention/__block_lambda_ prefix/schemas.sdstest @@ -0,0 +1,7 @@ +package tests.validation.nameConvention.blockLambdaPrefix + +// $TEST$ error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." +schema »__block_lambda_0« {} + +// $TEST$ no error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." +schema »_block_lambda_1« {} diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/segments.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/segments.sdstest new file mode 100644 index 000000000..3897af97e --- /dev/null +++ b/tests/resources/validation/name convention/__block_lambda_ prefix/segments.sdstest @@ -0,0 +1,7 @@ +package tests.validation.nameConvention.blockLambdaPrefix + +// $TEST$ error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." +segment »__block_lambda_0«() {} + +// $TEST$ no error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." +segment »_block_lambda_1«() {} diff --git a/tests/resources/validation/name convention/__block_lambda_ prefix/type parameters.sdstest b/tests/resources/validation/name convention/__block_lambda_ prefix/type parameters.sdstest new file mode 100644 index 000000000..218854599 --- /dev/null +++ b/tests/resources/validation/name convention/__block_lambda_ prefix/type parameters.sdstest @@ -0,0 +1,9 @@ +package tests.validation.nameConvention.blockLambdaPrefix + +fun myFunction3< + // $TEST$ error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." + »__block_lambda_0«, + + // $TEST$ no error "Names of declarations must not start with '__block_lambda_'. This is reserved for code generation of block lambdas." + »_block_lambda_1«, +>() diff --git a/tests/resources/validation/name convention/casing of declaration names/annotations.sdstest b/tests/resources/validation/name convention/casing of declaration names/annotations.sdstest new file mode 100644 index 000000000..bc083bd1b --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/annotations.sdstest @@ -0,0 +1,10 @@ +package tests.validation.declarations.nameCasing + +// $TEST$ no warning "Names of annotations should be UpperCamelCase." +annotation »AnnotationUppercase1« +// $TEST$ warning "Names of annotations should be UpperCamelCase." +annotation »annotationLowercase« +// $TEST$ warning "Names of annotations should be UpperCamelCase." +annotation »_annotationUnderscore« +// $TEST$ warning "Names of annotations should be UpperCamelCase." +annotation »Annotation_Snake_Case« diff --git a/tests/resources/validation/name convention/casing of declaration names/attributes.sdstest b/tests/resources/validation/name convention/casing of declaration names/attributes.sdstest new file mode 100644 index 000000000..3450363a8 --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/attributes.sdstest @@ -0,0 +1,12 @@ +package tests.validation.declarations.nameCasing + +class MyClass { + // $TEST$ warning "Names of attributes should be lowerCamelCase." + attr »AttributeUppercase«: Int + // $TEST$ no warning "Names of attributes should be lowerCamelCase." + attr »attributeLowercase1«: Int + // $TEST$ warning "Names of attributes should be lowerCamelCase." + attr »_attributeUnderscore«: Int + // $TEST$ warning "Names of attributes should be lowerCamelCase." + attr »attribute_snake_case«: Int +} diff --git a/tests/resources/validation/name convention/casing of declaration names/block lambda results.sdstest b/tests/resources/validation/name convention/casing of declaration names/block lambda results.sdstest new file mode 100644 index 000000000..165997092 --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/block lambda results.sdstest @@ -0,0 +1,14 @@ +package tests.validation.declarations.nameCasing + +pipeline myPipeline1 { + () { + // $TEST$ warning "Names of block lambda results should be lowerCamelCase." + yield »LambdaResultUppercase« = 1; + // $TEST$ no warning "Names of block lambda results should be lowerCamelCase." + yield »lambdaResultLowercase1« = 1; + // $TEST$ warning "Names of block lambda results should be lowerCamelCase." + yield »_lambdaResultUnderscore« = 1; + // $TEST$ warning "Names of block lambda results should be lowerCamelCase." + yield »lambdaResult_snake_case« = 1; + }; +} diff --git a/tests/resources/validation/name convention/casing of declaration names/classes.sdstest b/tests/resources/validation/name convention/casing of declaration names/classes.sdstest new file mode 100644 index 000000000..4a096b7f7 --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/classes.sdstest @@ -0,0 +1,10 @@ +package tests.validation.declarations.nameCasing + +// $TEST$ no warning "Names of classes should be UpperCamelCase." +class »ClassUppercase1« +// $TEST$ warning "Names of classes should be UpperCamelCase." +class »classLowercase« +// $TEST$ warning "Names of classes should be UpperCamelCase." +class »_classUnderscore« +// $TEST$ warning "Names of classes should be UpperCamelCase." +class »Class_Snake_Case« diff --git a/tests/resources/validation/name convention/casing of declaration names/enum variants.sdstest b/tests/resources/validation/name convention/casing of declaration names/enum variants.sdstest new file mode 100644 index 000000000..50571e29b --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/enum variants.sdstest @@ -0,0 +1,12 @@ +package tests.validation.declarations.nameCasing + +enum MyEnum { + // $TEST$ no warning "Names of enum variants should be UpperCamelCase." + »EnumVariantUppercase1« + // $TEST$ warning "Names of enum variants should be UpperCamelCase." + »enumVariantLowercase« + // $TEST$ warning "Names of enum variants should be UpperCamelCase." + »_enumVariantUnderscore« + // $TEST$ warning "Names of enum variants should be UpperCamelCase." + »Enum_Variant_Snake_Case« +} diff --git a/tests/resources/validation/name convention/casing of declaration names/enums.sdstest b/tests/resources/validation/name convention/casing of declaration names/enums.sdstest new file mode 100644 index 000000000..3f05ac478 --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/enums.sdstest @@ -0,0 +1,10 @@ +package tests.validation.declarations.nameCasing + +// $TEST$ no warning "Names of enums should be UpperCamelCase." +enum »EnumUppercase1« +// $TEST$ warning "Names of enums should be UpperCamelCase." +enum »enumLowercase« +// $TEST$ warning "Names of enums should be UpperCamelCase." +enum »_enumUnderscore« +// $TEST$ warning "Names of enums should be UpperCamelCase." +enum »Enum_Snake_Case« diff --git a/tests/resources/validation/name convention/casing of declaration names/functions.sdstest b/tests/resources/validation/name convention/casing of declaration names/functions.sdstest new file mode 100644 index 000000000..499367ba4 --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/functions.sdstest @@ -0,0 +1,10 @@ +package tests.validation.declarations.nameCasing + +// $TEST$ warning "Names of functions should be lowerCamelCase." +fun »FunctionUppercase«() +// $TEST$ no warning "Names of functions should be lowerCamelCase." +fun »functionLowercase1«() +// $TEST$ warning "Names of functions should be lowerCamelCase." +fun »_functionUnderscore«() +// $TEST$ warning "Names of functions should be lowerCamelCase." +fun »function_snake_case«() diff --git a/tests/resources/validation/name convention/casing of declaration names/package name leading underscore.sdstest b/tests/resources/validation/name convention/casing of declaration names/package name leading underscore.sdstest new file mode 100644 index 000000000..252c47d93 --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/package name leading underscore.sdstest @@ -0,0 +1,2 @@ +// $TEST$ warning "All segments of the qualified name of a package should be lowerCamelCase." +package »tests.validation.declarations._underscore« diff --git a/tests/resources/validation/name convention/casing of declaration names/package name lowercase.sdstest b/tests/resources/validation/name convention/casing of declaration names/package name lowercase.sdstest new file mode 100644 index 000000000..44069623d --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/package name lowercase.sdstest @@ -0,0 +1,2 @@ +// $TEST$ no warning "All segments of the qualified name of a package should be lowerCamelCase." +package »tests.validation.declarations.lowercase1« diff --git a/tests/resources/validation/name convention/casing of declaration names/package name snake case.sdstest b/tests/resources/validation/name convention/casing of declaration names/package name snake case.sdstest new file mode 100644 index 000000000..41197e773 --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/package name snake case.sdstest @@ -0,0 +1,2 @@ +// $TEST$ warning "All segments of the qualified name of a package should be lowerCamelCase." +package »tests.validation.declarations.snake_case« diff --git a/tests/resources/validation/name convention/casing of declaration names/package name uppercase.sdstest b/tests/resources/validation/name convention/casing of declaration names/package name uppercase.sdstest new file mode 100644 index 000000000..07272cee4 --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/package name uppercase.sdstest @@ -0,0 +1,2 @@ +// $TEST$ warning "All segments of the qualified name of a package should be lowerCamelCase." +package »tests.validation.declarations.Uppercase« diff --git a/tests/resources/validation/name convention/casing of declaration names/parameters.sdstest b/tests/resources/validation/name convention/casing of declaration names/parameters.sdstest new file mode 100644 index 000000000..0b225659a --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/parameters.sdstest @@ -0,0 +1,12 @@ +package tests.validation.declarations.nameCasing + +fun myFunction1( + // $TEST$ warning "Names of parameters should be lowerCamelCase." + »ParameterUppercase«: Int, + // $TEST$ no warning "Names of parameters should be lowerCamelCase." + »parameterLowercase1«: Int, + // $TEST$ warning "Names of parameters should be lowerCamelCase." + »_parameterUnderscore«: Int, + // $TEST$ warning "Names of parameters should be lowerCamelCase." + »parameter_snake_case«: Int +) diff --git a/tests/resources/validation/name convention/casing of declaration names/pipelines.sdstest b/tests/resources/validation/name convention/casing of declaration names/pipelines.sdstest new file mode 100644 index 000000000..2b5509623 --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/pipelines.sdstest @@ -0,0 +1,10 @@ +package tests.validation.declarations.nameCasing + +// $TEST$ warning "Names of pipelines should be lowerCamelCase." +pipeline »PipelineUppercase« {} +// $TEST$ no warning "Names of pipelines should be lowerCamelCase." +pipeline »pipelineLowercase1« {} +// $TEST$ warning "Names of pipelines should be lowerCamelCase." +pipeline »_pipelineUnderscore« {} +// $TEST$ warning "Names of pipelines should be lowerCamelCase." +pipeline »pipeline_snake_case« {} diff --git a/tests/resources/validation/name convention/casing of declaration names/placeholders.sdstest b/tests/resources/validation/name convention/casing of declaration names/placeholders.sdstest new file mode 100644 index 000000000..630b4ae54 --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/placeholders.sdstest @@ -0,0 +1,12 @@ +package tests.validation.declarations.nameCasing + +pipeline myPipeline2 { + // $TEST$ warning "Names of placeholders should be lowerCamelCase." + val »PlaceholderUppercase« = 1; + // $TEST$ no warning "Names of placeholders should be lowerCamelCase." + val »placeholderLowercase1« = 1; + // $TEST$ warning "Names of placeholders should be lowerCamelCase." + val »_placeholderUnderscore« = 1; + // $TEST$ warning "Names of placeholders should be lowerCamelCase." + val »placeholder_snake_case« = 1; +} diff --git a/tests/resources/validation/name convention/casing of declaration names/results.sdstest b/tests/resources/validation/name convention/casing of declaration names/results.sdstest new file mode 100644 index 000000000..73e9e6bd0 --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/results.sdstest @@ -0,0 +1,12 @@ +package tests.validation.declarations.nameCasing + +fun myFunction2() -> ( + // $TEST$ warning "Names of results should be lowerCamelCase." + »ResultUppercase«: Int, + // $TEST$ no warning "Names of results should be lowerCamelCase." + »resultLowercase1«: Int, + // $TEST$ warning "Names of results should be lowerCamelCase." + »_resultUnderscore«: Int, + // $TEST$ warning "Names of results should be lowerCamelCase." + »result_snake_case«: Int +) diff --git a/tests/resources/validation/name convention/casing of declaration names/schemas.sdstest b/tests/resources/validation/name convention/casing of declaration names/schemas.sdstest new file mode 100644 index 000000000..8ec1dc816 --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/schemas.sdstest @@ -0,0 +1,10 @@ +package tests.validation.declarations.nameCasing + +// $TEST$ no warning "Names of schemas should be UpperCamelCase." +schema »SchemaUppercase1« {} +// $TEST$ warning "Names of schemas should be UpperCamelCase." +schema »schemaLowercase« {} +// $TEST$ warning "Names of schemas should be UpperCamelCase." +schema »_schemaUnderscore« {} +// $TEST$ warning "Names of schemas should be UpperCamelCase." +schema »Schema_Snake_Case« {} diff --git a/tests/resources/validation/name convention/casing of declaration names/segments.sdstest b/tests/resources/validation/name convention/casing of declaration names/segments.sdstest new file mode 100644 index 000000000..400cf4bc6 --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/segments.sdstest @@ -0,0 +1,10 @@ +package tests.validation.declarations.nameCasing + +// $TEST$ warning "Names of segments should be lowerCamelCase." +segment »SegmentUppercase«() {} +// $TEST$ no warning "Names of segments should be lowerCamelCase." +segment »segmentLowercase1«() {} +// $TEST$ warning "Names of segments should be lowerCamelCase." +segment »_segmentUnderscore«() {} +// $TEST$ warning "Names of segments should be lowerCamelCase." +segment »segment_snake_case«() {} diff --git a/tests/resources/validation/name convention/casing of declaration names/type parameters.sdstest b/tests/resources/validation/name convention/casing of declaration names/type parameters.sdstest new file mode 100644 index 000000000..3ad52ffe9 --- /dev/null +++ b/tests/resources/validation/name convention/casing of declaration names/type parameters.sdstest @@ -0,0 +1,12 @@ +package tests.validation.declarations.nameCasing + +fun myFunction3< + // $TEST$ no warning "Names of type parameters should be UpperCamelCase." + »TypeParameterUppercase1«, + // $TEST$ warning "Names of type parameters should be UpperCamelCase." + »typeParameterLowercase«, + // $TEST$ warning "Names of type parameters should be UpperCamelCase." + »_typeParameterUnderscore«, + // $TEST$ warning "Names of type parameters should be UpperCamelCase." + »Type_Parameter_Snake_Case« +>()