Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ExtractedVariable,
extractExports,
extractFunctions,
extractVariables,
findAll,
findFirst,
findTopLevelConstants,
Expand All @@ -20,8 +21,7 @@ import {
SpecificObjectPropertyCall,
SpecificPropertyCall,
} from '@exercism/static-analysis'
import type { TSESTree } from '@typescript-eslint/typescript-estree'
import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree'
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree'
import { Source } from '~src/analyzers/SourceImpl'
import { parameterName } from '~src/analyzers/utils/extract_parameter'
import { assertNamedExport } from '~src/asserts/assert_named_export'
Expand Down Expand Up @@ -73,13 +73,18 @@ export class UnexpectedCallFound {
) {}
}

export class ShouldDefineTopLevelConstant {
constructor(public readonly name: string, public readonly value: string) {}
}

type Issue =
| undefined
| MissingExpectedCall
| HelperNotOptimal
| MethodNotFound
| HelperCallNotFound
| UnexpectedCallFound
| ShouldDefineTopLevelConstant

class Constant {
public readonly name: string
Expand Down Expand Up @@ -508,6 +513,15 @@ class Entry {
}
}

if (!constant) {
const issue = this.checkForShouldDefineTopLevelConstantIssue()
if (issue instanceof ShouldDefineTopLevelConstant) {
logger.log('~> found a constant that was not declared at the top level')
this.lastIssue_ = issue
return false
}
}

if (this.hasOneMap) {
logger.log('~> is a map solution')
return this.isOptimalMapSolution(logger, this.body, constant, program)
Expand Down Expand Up @@ -1111,6 +1125,20 @@ class Entry {
logger.log(`~> constant is not optimal`)
return false
}

private checkForShouldDefineTopLevelConstantIssue():
| ShouldDefineTopLevelConstant
| undefined {
const localConstants = extractVariables(this.body).filter(
(constant) =>
constant.init?.type === TSESTree.AST_NODE_TYPES.ArrayExpression ||
constant.init?.type === TSESTree.AST_NODE_TYPES.ObjectExpression
)
if (localConstants.length) {
const nameOfFirstConstant = localConstants[0].name || 'COLORS'
return new ShouldDefineTopLevelConstant(nameOfFirstConstant, '...')
}
}
}

export class ResistorColorDuoSolution {
Expand Down
23 changes: 23 additions & 0 deletions src/analyzers/practice/resistor-color-duo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
MethodNotFound,
MissingExpectedCall,
ResistorColorDuoSolution,
ShouldDefineTopLevelConstant,
UnexpectedCallFound,
} from './ResistorColorDuoSolution'

Expand Down Expand Up @@ -120,6 +121,21 @@ const ISSUE_UNEXPECTED_CALL = factory<'unexpected' | 'expected'>`
CommentType.Actionable
)

const PREFER_EXTRACTED_TOP_LEVEL_CONSTANT = factory<'value' | 'name'>`
Instead of defining the constant _inside_ the function, consider extracting it
to the top-level. Constants, functions, and classes that are not \`export\`ed,
are not accessible from outside the file.

\`\`\`javascript
const ${'name'} = ${'value'}

export const decodedValue = (...)
\`\`\`
`(
'javascript.resistor-color-duo.prefer_extracted_top_level_constant',
CommentType.Actionable
)

type Program = TSESTree.Program

export class ResistorColorDuoAnalyzer extends IsolatedAnalyzerImpl {
Expand Down Expand Up @@ -275,6 +291,13 @@ export class ResistorColorDuoAnalyzer extends IsolatedAnalyzerImpl {
}

output.disapprove()
} else if (lastIssue instanceof ShouldDefineTopLevelConstant) {
output.add(
PREFER_EXTRACTED_TOP_LEVEL_CONSTANT({
name: lastIssue.name,
value: lastIssue.value,
})
)
} else {
this.logger.error(
'The analyzer did not handle the issue: ' + JSON.stringify(lastIssue)
Expand Down
Loading