diff --git a/src/application/Parser/Executable/Script/Compiler/ScriptCompilerFactory.ts b/src/application/Parser/Executable/Script/Compiler/ScriptCompilerFactory.ts index 1ed34dcf..a59b2d2f 100644 --- a/src/application/Parser/Executable/Script/Compiler/ScriptCompilerFactory.ts +++ b/src/application/Parser/Executable/Script/Compiler/ScriptCompilerFactory.ts @@ -80,7 +80,7 @@ class FunctionCallScriptCompiler implements ScriptCompiler { } const calls = parseFunctionCalls(script.call); const compiledCode = this.utilities.callCompiler.compileFunctionCalls(calls, this.functions); - validateCompiledCode( + validateFinalCompiledCode( compiledCode, this.language, this.utilities.codeValidator, @@ -95,7 +95,7 @@ class FunctionCallScriptCompiler implements ScriptCompiler { } } -function validateCompiledCode( +function validateFinalCompiledCode( compiledCode: CompiledCode, language: ScriptingLanguage, validate: CodeValidator, @@ -109,6 +109,7 @@ function validateCompiledCode( CodeValidationRule.NoEmptyLines, CodeValidationRule.NoTooLongLines, // Allow duplicated lines to enable calling same function multiple times + CodeValidationRule.NoCommentOnlyLines, ], ), ); diff --git a/src/application/Parser/Executable/Script/ScriptParser.ts b/src/application/Parser/Executable/Script/ScriptParser.ts index 05e6b8a8..010d83f4 100644 --- a/src/application/Parser/Executable/Script/ScriptParser.ts +++ b/src/application/Parser/Executable/Script/ScriptParser.ts @@ -95,6 +95,7 @@ function validateHardcodedCodeWithoutCalls( CodeValidationRule.NoEmptyLines, CodeValidationRule.NoDuplicatedLines, CodeValidationRule.NoTooLongLines, + CodeValidationRule.NoCommentOnlyLines, ], ), ); diff --git a/src/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeCommentOnlyCode.ts b/src/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeCommentOnlyCode.ts new file mode 100644 index 00000000..b2898be5 --- /dev/null +++ b/src/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeCommentOnlyCode.ts @@ -0,0 +1,33 @@ +import type { ScriptingLanguage } from '@/domain/ScriptingLanguage'; +import { isCommentLine, type CommentLineChecker } from './Common/CommentLineChecker'; +import { createSyntax, type SyntaxFactory } from './Syntax/SyntaxFactory'; +import type { CodeLine, CodeValidationAnalyzer, InvalidCodeLine } from './CodeValidationAnalyzer'; + +export type CommentOnlyCodeAnalyzer = CodeValidationAnalyzer & { + ( + ...args: [ + ...Parameters, + syntaxFactory?: SyntaxFactory, + commentLineChecker?: CommentLineChecker, + ] + ): ReturnType; +}; + +export const analyzeCommentOnlyCode: CommentOnlyCodeAnalyzer = ( + lines: readonly CodeLine[], + language: ScriptingLanguage, + syntaxFactory: SyntaxFactory = createSyntax, + commentLineChecker: CommentLineChecker = isCommentLine, +) => { + const syntax = syntaxFactory(language); + if (!lines.every((line) => commentLineChecker(line.text, syntax))) { + return []; + } + return lines + .map((line): InvalidCodeLine => ({ + lineNumber: line.lineNumber, + error: (() => { + return 'Code consists of comments only'; + })(), + })); +}; diff --git a/src/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeDuplicateLines.ts b/src/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeDuplicateLines.ts index b41de254..0bb7378f 100644 --- a/src/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeDuplicateLines.ts +++ b/src/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeDuplicateLines.ts @@ -1,6 +1,7 @@ import type { LanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/LanguageSyntax'; import type { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { createSyntax, type SyntaxFactory } from './Syntax/SyntaxFactory'; +import { isCommentLine, type CommentLineChecker } from './Common/CommentLineChecker'; import type { CodeLine, CodeValidationAnalyzer, InvalidCodeLine } from './CodeValidationAnalyzer'; export type DuplicateLinesAnalyzer = CodeValidationAnalyzer & { @@ -8,6 +9,7 @@ export type DuplicateLinesAnalyzer = CodeValidationAnalyzer & { ...args: [ ...Parameters, syntaxFactory?: SyntaxFactory, + commentLineChecker?: CommentLineChecker, ] ): ReturnType; }; @@ -16,12 +18,13 @@ export const analyzeDuplicateLines: DuplicateLinesAnalyzer = ( lines: readonly CodeLine[], language: ScriptingLanguage, syntaxFactory: SyntaxFactory = createSyntax, + commentLineChecker: CommentLineChecker = isCommentLine, ) => { const syntax = syntaxFactory(language); return lines .map((line): CodeLineWithDuplicateOccurrences => ({ lineNumber: line.lineNumber, - shouldBeIgnoredInAnalysis: shouldIgnoreLine(line.text, syntax), + shouldBeIgnoredInAnalysis: shouldIgnoreLine(line.text, syntax, commentLineChecker), duplicateLineNumbers: lines .filter((other) => other.text === line.text) .map((duplicatedLine) => duplicatedLine.lineNumber), @@ -43,17 +46,15 @@ function isNonIgnorableDuplicateLine(line: CodeLineWithDuplicateOccurrences): bo return !line.shouldBeIgnoredInAnalysis && line.duplicateLineNumbers.length > 1; } -function shouldIgnoreLine(codeLine: string, syntax: LanguageSyntax): boolean { - return isCommentLine(codeLine, syntax) +function shouldIgnoreLine( + codeLine: string, + syntax: LanguageSyntax, + commentLineChecker: CommentLineChecker, +): boolean { + return commentLineChecker(codeLine, syntax) || isLineComposedEntirelyOfCommonCodeParts(codeLine, syntax); } -function isCommentLine(codeLine: string, syntax: LanguageSyntax): boolean { - return syntax.commentDelimiters.some( - (delimiter) => codeLine.startsWith(delimiter), - ); -} - function isLineComposedEntirelyOfCommonCodeParts( codeLine: string, syntax: LanguageSyntax, diff --git a/src/application/Parser/Executable/Script/Validation/Analyzers/Common/CommentLineChecker.ts b/src/application/Parser/Executable/Script/Validation/Analyzers/Common/CommentLineChecker.ts new file mode 100644 index 00000000..3555be57 --- /dev/null +++ b/src/application/Parser/Executable/Script/Validation/Analyzers/Common/CommentLineChecker.ts @@ -0,0 +1,14 @@ +import type { LanguageSyntax } from '../Syntax/LanguageSyntax'; + +export interface CommentLineChecker { + ( + codeLine: string, + syntax: LanguageSyntax, + ): boolean; +} + +export const isCommentLine: CommentLineChecker = (codeLine, syntax) => { + return syntax.commentDelimiters.some( + (delimiter) => codeLine.toLowerCase().startsWith(delimiter.toLowerCase()), + ); +}; diff --git a/src/application/Parser/Executable/Script/Validation/CodeValidationRule.ts b/src/application/Parser/Executable/Script/Validation/CodeValidationRule.ts index a1206348..c5888e96 100644 --- a/src/application/Parser/Executable/Script/Validation/CodeValidationRule.ts +++ b/src/application/Parser/Executable/Script/Validation/CodeValidationRule.ts @@ -2,4 +2,5 @@ export enum CodeValidationRule { NoEmptyLines, NoDuplicatedLines, NoTooLongLines, + NoCommentOnlyLines, } diff --git a/src/application/Parser/Executable/Script/Validation/ValidationRuleAnalyzerFactory.ts b/src/application/Parser/Executable/Script/Validation/ValidationRuleAnalyzerFactory.ts index ac906f09..bd63f5fa 100644 --- a/src/application/Parser/Executable/Script/Validation/ValidationRuleAnalyzerFactory.ts +++ b/src/application/Parser/Executable/Script/Validation/ValidationRuleAnalyzerFactory.ts @@ -2,6 +2,7 @@ import { CodeValidationRule } from './CodeValidationRule'; import { analyzeDuplicateLines } from './Analyzers/AnalyzeDuplicateLines'; import { analyzeEmptyLines } from './Analyzers/AnalyzeEmptyLines'; import { analyzeTooLongLines } from './Analyzers/AnalyzeTooLongLines'; +import { analyzeCommentOnlyCode } from './Analyzers/AnalyzeCommentOnlyCode'; import type { CodeValidationAnalyzer } from './Analyzers/CodeValidationAnalyzer'; export interface ValidationRuleAnalyzerFactory { @@ -26,6 +27,8 @@ function createValidationRule(rule: CodeValidationRule): CodeValidationAnalyzer return analyzeDuplicateLines; case CodeValidationRule.NoTooLongLines: return analyzeTooLongLines; + case CodeValidationRule.NoCommentOnlyLines: + return analyzeCommentOnlyCode; default: throw new Error(`Unknown rule: ${rule}`); } diff --git a/src/application/collections/windows.yaml b/src/application/collections/windows.yaml index 63a51060..711c1f90 100644 --- a/src/application/collections/windows.yaml +++ b/src/application/collections/windows.yaml @@ -40136,7 +40136,9 @@ functions: Remove the registry key "{{ $keyPath }}" {{ with $codeComment }}({{ . }}){{ end }} revertCodeComment: >- - Recreate the registry key "{{ $keyPath }}" + {{ with $recreateOnRevert }} + Recreate the registry key "{{ $keyPath }}" + {{ end }} - # Marked: refactor-with-variables # - Replacing SID is same as `CreateRegistryKey` diff --git a/tests/unit/application/Parser/Executable/Script/Compiler/ScriptCompilerFactory.spec.ts b/tests/unit/application/Parser/Executable/Script/Compiler/ScriptCompilerFactory.spec.ts index d3a4b81d..9f7ab869 100644 --- a/tests/unit/application/Parser/Executable/Script/Compiler/ScriptCompilerFactory.spec.ts +++ b/tests/unit/application/Parser/Executable/Script/Compiler/ScriptCompilerFactory.spec.ts @@ -234,6 +234,7 @@ describe('ScriptCompilerFactory', () => { CodeValidationRule.NoEmptyLines, CodeValidationRule.NoTooLongLines, // Allow duplicated lines to enable calling same function multiple times + CodeValidationRule.NoCommentOnlyLines, ]; const scriptData = createScriptDataWithCall(); const validator = new CodeValidatorStub(); diff --git a/tests/unit/application/Parser/Executable/Script/ScriptParser.spec.ts b/tests/unit/application/Parser/Executable/Script/ScriptParser.spec.ts index 05f93532..b515b638 100644 --- a/tests/unit/application/Parser/Executable/Script/ScriptParser.spec.ts +++ b/tests/unit/application/Parser/Executable/Script/ScriptParser.spec.ts @@ -389,6 +389,7 @@ describe('ScriptParser', () => { CodeValidationRule.NoEmptyLines, CodeValidationRule.NoDuplicatedLines, CodeValidationRule.NoTooLongLines, + CodeValidationRule.NoCommentOnlyLines, ]; const validator = new CodeValidatorStub(); // act diff --git a/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeCommentOnlyCode.spec.ts b/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeCommentOnlyCode.spec.ts new file mode 100644 index 00000000..0f39e117 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeCommentOnlyCode.spec.ts @@ -0,0 +1,133 @@ +import { describe, it } from 'vitest'; +import { analyzeCommentOnlyCode, type CommentOnlyCodeAnalyzer } from '@/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeCommentOnlyCode'; +import type { CommentLineChecker } from '@/application/Parser/Executable/Script/Validation/Analyzers/Common/CommentLineChecker'; +import type { SyntaxFactory } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/SyntaxFactory'; +import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; +import type { CodeLine, InvalidCodeLine } from '@/application/Parser/Executable/Script/Validation/Analyzers/CodeValidationAnalyzer'; +import { CommentLineCheckerStub } from '@tests/unit/shared/Stubs/CommentLineCheckerStub'; +import { SyntaxFactoryStub } from '@tests/unit/shared/Stubs/SyntaxFactoryStub'; +import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub'; +import { createCodeLines } from './CreateCodeLines'; +import { expectSameInvalidCodeLines } from './ExpectSameInvalidCodeLines'; + +describe('AnalyzeCommentOnlyCode', () => { + describe('analyzeCommentOnlyCode', () => { + it('returns empty given no match', () => { + // arrange + const context = setupScenario({ + givenLines: ['line-1', 'line-2', 'line-3'], + matchedLines: [], + }); + // act + const actualResult = context.analyze(); + // assert + expect(actualResult).to.have.lengthOf(0); + }); + it('returns empty given some matches', () => { + // arrange + const context = setupScenario({ + givenLines: ['line-1', 'line-2'], + matchedLines: [], + }); + // act + const actualResult = context.analyze(); + // assert + expect(actualResult).to.have.lengthOf(0); + }); + it('returns all lines given all match', () => { + // arrange + const lines = ['line-1', 'line-2', 'line-3']; + const expectedResult: InvalidCodeLine[] = lines + .map((_line, index): InvalidCodeLine => ({ + lineNumber: index + 1, + error: 'Code consists of comments only', + })); + const context = setupScenario({ + givenLines: lines, + matchedLines: lines, + }); + // act + const actualResult = context.analyze(); + // assert + expectSameInvalidCodeLines(expectedResult, actualResult); + }); + it('uses correct language for syntax creation', () => { + // arrange + const expectedLanguage = ScriptingLanguage.batchfile; + let actualLanguage: ScriptingLanguage | undefined; + const factory: SyntaxFactory = (language) => { + actualLanguage = language; + return new LanguageSyntaxStub(); + }; + const context = new TestContext() + .withLanguage(expectedLanguage) + .withSyntaxFactory(factory); + // act + context.analyze(); + // assert + expect(actualLanguage).to.equal(expectedLanguage); + }); + }); +}); + +interface CommentOnlyCodeAnalysisTestScenario { + readonly givenLines: readonly string[]; + readonly matchedLines: readonly string[]; +} + +function setupScenario( + scenario: CommentOnlyCodeAnalysisTestScenario, +): TestContext { + // arrange + const lines = scenario.givenLines; + const syntax = new LanguageSyntaxStub(); + const checker = new CommentLineCheckerStub(); + scenario.matchedLines.forEach((line) => checker.withPredeterminedResult({ + givenLine: line, + givenSyntax: syntax, + result: true, + })); + return new TestContext() + .withSyntaxFactory(() => syntax) + .withLines(lines) + .withCommentLineChecker(checker.get()); +} + +export class TestContext { + private codeLines: readonly CodeLine[] = createCodeLines(['test-code-line']); + + private language = ScriptingLanguage.batchfile; + + private syntaxFactory: SyntaxFactory = new SyntaxFactoryStub().get(); + + private commentLineChecker: CommentLineChecker = new CommentLineCheckerStub().get(); + + public withLines(lines: readonly string[]): this { + this.codeLines = createCodeLines(lines); + return this; + } + + public withLanguage(language: ScriptingLanguage): this { + this.language = language; + return this; + } + + public withSyntaxFactory(syntaxFactory: SyntaxFactory): this { + this.syntaxFactory = syntaxFactory; + return this; + } + + public withCommentLineChecker(commentLineChecker: CommentLineChecker): this { + this.commentLineChecker = commentLineChecker; + return this; + } + + public analyze(): ReturnType { + return analyzeCommentOnlyCode( + this.codeLines, + this.language, + this.syntaxFactory, + this.commentLineChecker, + ); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeDuplicateLines.spec.ts b/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeDuplicateLines.spec.ts index 6694994f..cc4ce962 100644 --- a/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeDuplicateLines.spec.ts +++ b/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeDuplicateLines.spec.ts @@ -1,10 +1,13 @@ -import { describe } from 'vitest'; -import { analyzeDuplicateLines } from '@/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeDuplicateLines'; +import { describe, it } from 'vitest'; +import { analyzeDuplicateLines, type DuplicateLinesAnalyzer } from '@/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeDuplicateLines'; import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub'; import type { CodeLine, InvalidCodeLine } from '@/application/Parser/Executable/Script/Validation/Analyzers/CodeValidationAnalyzer'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import type { SyntaxFactory } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/SyntaxFactory'; import { SyntaxFactoryStub } from '@tests/unit/shared/Stubs/SyntaxFactoryStub'; +import type { CommentLineChecker } from '@/application/Parser/Executable/Script/Validation/Analyzers/Common/CommentLineChecker'; +import { CommentLineCheckerStub } from '@tests/unit/shared/Stubs/CommentLineCheckerStub'; +import type { LanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/LanguageSyntax'; import { createCodeLines } from './CreateCodeLines'; import { expectSameInvalidCodeLines } from './ExpectSameInvalidCodeLines'; @@ -116,34 +119,45 @@ describe('AnalyzeDuplicateLines', () => { }); }); describe('comment handling', () => { - it('ignores lines starting with comment delimiters', () => { + it('uses correct syntax for comment checking', () => { // arrange - const expected = createExpectedDuplicateLineErrors([3, 5]); - const syntax = new LanguageSyntaxStub() - .withCommentDelimiters('#', '//'); + const expectedSyntax = new LanguageSyntaxStub(); + const syntaxFactory: SyntaxFactory = () => { + return expectedSyntax; + }; + let actualSyntax: LanguageSyntax | undefined; + const commentChecker: CommentLineChecker = (_, syntax) => { + actualSyntax = syntax; + return false; + }; const context = new TestContext() - .withLines([ - /* 1 */ '#abc', /* 2 */ '#abc', /* 3 */ 'abc', /* 4 */ 'unique', - /* 5 */ 'abc', /* 6 */ '//abc', /* 7 */ '//abc', /* 8 */ '//unique', - /* 9 */ '#unique', - ]) - .withSyntaxFactory(() => syntax); + .withCommentLineChecker(commentChecker) + .withSyntaxFactory((syntaxFactory)) + .withLines(['single test line']); // act - const actual = context.analyze(); + context.analyze(); // assert - expectSameInvalidCodeLines(actual, expected); + expect(actualSyntax).to.equal(expectedSyntax); }); - it('detects duplicates when comments are not at line start', () => { + it('ignores comment lines', () => { // arrange - const expected = createExpectedDuplicateLineErrors([1, 2], [3, 4]); + const commentLine = 'duplicate-comment-line'; + const expected = createExpectedDuplicateLineErrors([3, 5]); const syntax = new LanguageSyntaxStub() - .withCommentDelimiters('#'); + .withCommentDelimiters('#', '//'); + const commentChecker = new CommentLineCheckerStub() + .withPredeterminedResult({ + givenLine: commentLine, + givenSyntax: syntax, + result: true, + }).get(); const context = new TestContext() .withLines([ - /* 1 */ 'test #comment', /* 2 */ 'test #comment', /* 3 */ 'test2 # comment', - /* 4 */ 'test2 # comment', + /* 1 */ commentLine, /* 2 */ commentLine, /* 3 */ 'abc', /* 4 */ 'unique', + /* 5 */ 'abc', /* 6 */ commentLine, ]) - .withSyntaxFactory(() => syntax); + .withSyntaxFactory(() => syntax) + .withCommentLineChecker(commentChecker); // act const actual = context.analyze(); // assert @@ -171,6 +185,8 @@ export class TestContext { private syntaxFactory: SyntaxFactory = new SyntaxFactoryStub().get(); + private commentLineChecker: CommentLineChecker = new CommentLineCheckerStub().get(); + public withLines(lines: readonly string[]): this { this.codeLines = createCodeLines(lines); return this; @@ -186,11 +202,17 @@ export class TestContext { return this; } - public analyze() { + public withCommentLineChecker(commentLineChecker: CommentLineChecker): this { + this.commentLineChecker = commentLineChecker; + return this; + } + + public analyze(): ReturnType { return analyzeDuplicateLines( this.codeLines, this.language, this.syntaxFactory, + this.commentLineChecker, ); } } diff --git a/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/Common/CommentLineChecker.spec.ts b/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/Common/CommentLineChecker.spec.ts new file mode 100644 index 00000000..51564f20 --- /dev/null +++ b/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/Common/CommentLineChecker.spec.ts @@ -0,0 +1,132 @@ +import { describe, it } from 'vitest'; +import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub'; +import { isCommentLine, type CommentLineChecker } from '@/application/Parser/Executable/Script/Validation/Analyzers/Common/CommentLineChecker'; +import type { LanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/LanguageSyntax'; + +describe('CommentLineChecker', () => { + describe('isCommentLine', () => { + interface CommentLineTestScenario { + readonly description: string; + readonly expectedResult: boolean; + readonly line: string; + readonly commentDelimiters: readonly string[]; + } + const testScenarios: readonly CommentLineTestScenario[] = [ + { + description: 'returns true for line starting with single character comment', + expectedResult: true, + line: '#test', + commentDelimiters: ['#'], + }, + { + description: 'returns true for line with only comment and whitespace', + expectedResult: true, + line: '# ', + commentDelimiters: ['#'], + }, + { + description: 'returns true for multi-character comment prefix', + expectedResult: true, + line: 'delimiter code', + commentDelimiters: ['delimiter'], + }, + { + description: 'returns true for line with only comment delimiter', + expectedResult: true, + line: '//', + commentDelimiters: ['//'], + }, + { + description: 'returns true when matching any configured delimiter', + expectedResult: true, + line: '// test', + commentDelimiters: ['#', '//'], + }, + { + description: 'return true for case-sensitive delimiters', + expectedResult: true, + line: 'REM this is still a comment', + commentDelimiters: ['rem'], + }, + { + description: 'returns false for line without comment prefix', + expectedResult: false, + line: 'test', + commentDelimiters: ['#', '//'], + }, + { + description: 'returns false when comment appears mid-line', + expectedResult: false, + line: 'test // code', + commentDelimiters: ['//'], + }, + { + description: 'returns false when comment appears at end', + expectedResult: false, + line: 'code //', + commentDelimiters: ['//'], + }, + { + description: 'returns false for line starting with whitespace', + expectedResult: false, + line: ' // code', + commentDelimiters: ['//'], + }, + { + description: 'returns false when input contains partial delimiter', + expectedResult: false, + line: '// code', + commentDelimiters: ['///'], + }, + { + description: 'returns false when no delimiters are configured', + expectedResult: false, + line: '// code', + commentDelimiters: [], + }, + { + description: 'returns false for empty line', + expectedResult: false, + line: '', + commentDelimiters: ['#', '//'], + }, + ]; + testScenarios.forEach((scenario) => { + it(scenario.description, () => { + // arrange + const { expectedResult } = scenario; + const context = new TestContext() + .withLine(scenario.line) + .withSyntax(new LanguageSyntaxStub() + .withCommentDelimiters(...scenario.commentDelimiters)); + // act + const actualResult = context.check(); + // assert + expect(actualResult).to.equal(expectedResult); + }); + }); + }); +}); + +export class TestContext { + private codeLine = `[${TestContext}] test code line`; + + private syntax: LanguageSyntax = new LanguageSyntaxStub(); + + public withLine(codeLine: string): this { + this.codeLine = codeLine; + return this; + } + + public withSyntax(syntax: LanguageSyntax): this { + this.syntax = syntax; + return this; + } + + public check(): ReturnType { + return isCommentLine( + this.codeLine, + this.syntax, + ); + } +} diff --git a/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/ExpectSameInvalidCodeLines.ts b/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/ExpectSameInvalidCodeLines.ts index dd99e3c2..3e3fdc18 100644 --- a/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/ExpectSameInvalidCodeLines.ts +++ b/tests/unit/application/Parser/Executable/Script/Validation/Analyzers/ExpectSameInvalidCodeLines.ts @@ -1,6 +1,7 @@ import { expect } from 'vitest'; import type { InvalidCodeLine } from '@/application/Parser/Executable/Script/Validation/Analyzers/CodeValidationAnalyzer'; import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; +import { indentText } from '@/application/Common/Text/IndentText'; export function expectSameInvalidCodeLines( expected: readonly InvalidCodeLine[], @@ -11,8 +12,8 @@ export function expectSameInvalidCodeLines( expect(sortedActual).to.deep.equal(sortedExpected, formatAssertionMessage([ 'Invalid code lines do not match', - `Expected: ${JSON.stringify(sortedExpected, null, 2)}`, - `Actual: ${JSON.stringify(sortedActual, null, 2)}`, + `Expected:\n${indentText(printLines(sortedExpected))}`, + `Actual:\n${indentText(printLines(sortedActual))}`, ])); } @@ -32,3 +33,11 @@ export function expectInvalidCodeLines( function sort(lines: readonly InvalidCodeLine[]) { // To ignore order return Array.from(lines).sort((a, b) => a.lineNumber - b.lineNumber); } + +function printLines(lines: readonly InvalidCodeLine[]): string { + return [ + `Total items: ${lines.length === 0 ? '0 (Empty)' : lines.length}`, + 'As JSON:', + indentText(JSON.stringify(lines, null, 2)), + ].join('\n'); +} diff --git a/tests/unit/application/Parser/Executable/Script/Validation/ValidationRuleAnalyzerFactory.spec.ts b/tests/unit/application/Parser/Executable/Script/Validation/ValidationRuleAnalyzerFactory.spec.ts index 8358bd7f..afbd1226 100644 --- a/tests/unit/application/Parser/Executable/Script/Validation/ValidationRuleAnalyzerFactory.spec.ts +++ b/tests/unit/application/Parser/Executable/Script/Validation/ValidationRuleAnalyzerFactory.spec.ts @@ -6,6 +6,7 @@ import { analyzeTooLongLines } from '@/application/Parser/Executable/Script/Vali import { createValidationAnalyzers } from '@/application/Parser/Executable/Script/Validation/ValidationRuleAnalyzerFactory'; import type { CodeValidationAnalyzer } from '@/application/Parser/Executable/Script/Validation/Analyzers/CodeValidationAnalyzer'; import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; +import { analyzeCommentOnlyCode } from '@/application/Parser/Executable/Script/Validation/Analyzers/AnalyzeCommentOnlyCode'; describe('ValidationRuleAnalyzerFactory', () => { describe('createValidationAnalyzers', () => { @@ -27,6 +28,7 @@ describe('ValidationRuleAnalyzerFactory', () => { [CodeValidationRule.NoEmptyLines]: analyzeEmptyLines, [CodeValidationRule.NoDuplicatedLines]: analyzeDuplicateLines, [CodeValidationRule.NoTooLongLines]: analyzeTooLongLines, + [CodeValidationRule.NoCommentOnlyLines]: analyzeCommentOnlyCode, }; const givenRules: CodeValidationRule[] = Object .keys(expectedAnalyzersForRules) diff --git a/tests/unit/shared/Stubs/CommentLineCheckerStub.ts b/tests/unit/shared/Stubs/CommentLineCheckerStub.ts new file mode 100644 index 00000000..d9cbb25d --- /dev/null +++ b/tests/unit/shared/Stubs/CommentLineCheckerStub.ts @@ -0,0 +1,31 @@ +import type { LanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Analyzers/Syntax/LanguageSyntax'; +import type { CommentLineChecker } from '@/application/Parser/Executable/Script/Validation/Analyzers/Common/CommentLineChecker'; + +interface PredeterminedResult { + readonly givenLine: string; + readonly givenSyntax: LanguageSyntax; + readonly result: boolean; +} + +export class CommentLineCheckerStub { + private readonly predeterminedResults = new Array(); + + public withPredeterminedResult(scenario: PredeterminedResult): this { + this.predeterminedResults.push(scenario); + return this; + } + + public get(): CommentLineChecker { + return (line: string, syntax: LanguageSyntax): boolean => { + const results = this.predeterminedResults + .filter((r) => r.givenLine === line && r.givenSyntax === syntax); + if (results.length === 0) { + return false; + } + if (results.length > 1) { + throw new Error(`Logical error: More than single predetermined results for line "${line}"`); + } + return results[0].result; + }; + } +}