diff --git a/src/analyzers/practice/two-fer/index.ts b/src/analyzers/practice/two-fer/index.ts index 6203d186..5b49a5bb 100644 --- a/src/analyzers/practice/two-fer/index.ts +++ b/src/analyzers/practice/two-fer/index.ts @@ -36,6 +36,8 @@ import { import { extractNamedFunction } from '~src/extracts/extract_named_function' import { makeNoSourceOutput } from '~src/output/makeNoSourceOutput' import { makeParseErrorOutput } from '~src/output/makeParseErrorOutput' +import { hasStubThrow } from '~src/analyzers/utils/extract_main_method' +import { REMOVE_STUB_THROW } from '~src/comments/remove_stub_throw' type ConditionalExpression = TSESTree.ConditionalExpression type IfStatement = TSESTree.IfStatement @@ -82,7 +84,7 @@ export class TwoFerAnalyzer extends AnalyzerImpl { private program!: Program private source!: string - private mainMethod!: ExtractedFunction + private mainMethod?: ExtractedFunction protected async execute(input: Input): Promise { const [parsed] = await this.parse(input) @@ -90,11 +92,17 @@ export class TwoFerAnalyzer extends AnalyzerImpl { this.program = parsed.program this.source = parsed.source - this.mainMethod = extractNamedFunction('twoFer', this.program)! + this.mainMethod = extractNamedFunction('twoFer', this.program) // Firstly we want to check that the structure of this solution is correct // and that there is nothing structural stopping it from passing the tests this.checkStructure() + if (!this.mainMethod) return + + if (hasStubThrow(this.mainMethod)) { + this.disapprove(REMOVE_STUB_THROW()) + return + } // Now we want to ensure that the method signature is sane and that it has // valid arguments diff --git a/src/analyzers/utils/extract_main_method.ts b/src/analyzers/utils/extract_main_method.ts index 6232d55b..a49ad33c 100644 --- a/src/analyzers/utils/extract_main_method.ts +++ b/src/analyzers/utils/extract_main_method.ts @@ -79,3 +79,41 @@ export function extractMainMethod( return undefined } + +function isNewExpression(node: unknown): node is TSESTree.NewExpression { + return ( + typeof node === 'object' && + node !== null && + (node as TSESTree.Node).type === 'NewExpression' + ) +} + +function isStubThrowStatement(statement: TSESTree.Statement): boolean { + if (statement.type !== 'ThrowStatement') return false + + const argument = statement.argument + if (!isNewExpression(argument)) return false + + const callee = argument.callee + if (callee.type !== 'Identifier' || callee.name !== 'Error') return false + + const [firstArg] = argument.arguments + if (!firstArg || firstArg.type !== 'Literal') return false + if (typeof firstArg.value !== 'string') return false + + return ( + firstArg.value.includes('Please implement') || + firstArg.value.includes('Remove this line and implement') || + firstArg.value.includes('Implement the') || + firstArg.value.includes('Remove this statement and implement') + ) +} + +export function hasStubThrow(fn: { body?: TSESTree.Node }): boolean { + if (!fn.body || fn.body.type !== 'BlockStatement') return false + + return fn.body.body.some( + (statement) => + statement.type === 'ThrowStatement' && isStubThrowStatement(statement) + ) +} diff --git a/src/comments/remove_stub_throw.ts b/src/comments/remove_stub_throw.ts new file mode 100644 index 00000000..a0f4eae4 --- /dev/null +++ b/src/comments/remove_stub_throw.ts @@ -0,0 +1,5 @@ +import { factory, CommentType } from '~src/comments/comment' + +export const REMOVE_STUB_THROW = factory` +Remove this placeholder throw statement. It is dead code. +`('javascript.general.remove_stub_throw', CommentType.Essential) diff --git a/test/analyzers/two-fer/stub-throw.ts b/test/analyzers/two-fer/stub-throw.ts new file mode 100644 index 00000000..01472718 --- /dev/null +++ b/test/analyzers/two-fer/stub-throw.ts @@ -0,0 +1,43 @@ +import { TwoFerAnalyzer } from '~src/analyzers/practice/two-fer' +import { makeAnalyze } from '~test/helpers/smoke' + +const analyze = makeAnalyze(() => new TwoFerAnalyzer()) + +describe('two-fer stub throw detection', () => { + it('blocks canonical exercism stub throw', async () => { + const solution = ` + export function twoFer(name = 'you') { + return 'One for you, one for me.' + throw new Error('Remove this line and implement the function'); + } + `.trim() + + const output = await analyze(solution) + + const stub = output.comments.find( + (c) => c.externalTemplate === 'javascript.general.remove_stub_throw' + ) + + expect(stub).toBeDefined() + expect(stub?.type).toBe('essential') + }) + + it('does not block normal error throws', async () => { + const solution = ` + export function twoFer(name = 'you') { + if (name === 'bad') { + throw new Error('Invalid name'); + } + return \`One for \${name}, one for me.\` + } + `.trim() + + const output = await analyze(solution) + + const stub = output.comments.find( + (c) => c.externalTemplate === 'javascript.general.remove_stub_throw' + ) + + expect(stub).toBeUndefined() + }) +})