Skip to content

Commit e115f6d

Browse files
Add exemplar analyzers (#84)
1 parent 00d941f commit e115f6d

File tree

18 files changed

+873
-4
lines changed

18 files changed

+873
-4
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 0.12.0
4+
5+
- Add generic ExemplarAnalyzer
6+
- Use generic ExemplarAnalyzer for `annalyns-infiltration`
7+
- Use generic ExemplarAnalyzer for `freelancer-rates`
8+
39
## 0.11.2
410

511
- Improve `concept/lasagna` analysis

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@exercism/javascript-analyzer",
3-
"version": "0.11.2",
3+
"version": "0.12.0",
44
"description": "Exercism analyzer for javascript",
55
"repository": "https://github.com/exercism/javascript-analyzer",
66
"author": "Derk-Jan Karrenbeld <[email protected]>",
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
AstParser,
3+
extractExports,
4+
extractFunctions,
5+
} from '@exercism/static-analysis'
6+
import { TSESTree } from '@typescript-eslint/typescript-estree'
7+
import { readFileSync } from 'fs'
8+
import path from 'path'
9+
import { Source } from '../../SourceImpl'
10+
11+
export class ExemplarSolution {
12+
private readonly source: Source
13+
14+
private exemplar!: Source
15+
16+
constructor(public readonly program: TSESTree.Program, source: string) {
17+
this.source = new Source(source)
18+
19+
const functions = extractFunctions(program)
20+
const exports = extractExports(program)
21+
}
22+
23+
public readExemplar(directory: string): void {
24+
const configPath = path.join(directory, '.meta', 'config.json')
25+
const config = JSON.parse(readFileSync(configPath).toString())
26+
27+
const exemplarPath = path.join(directory, config.files.exemplar[0])
28+
this.exemplar = new Source(readFileSync(exemplarPath).toString())
29+
}
30+
31+
public get isExemplar(): boolean {
32+
const sourceAst = AstParser.REPRESENTER.parseSync(this.source.toString())
33+
const exemplarAst = AstParser.REPRESENTER.parseSync(
34+
this.exemplar.toString()
35+
)
36+
37+
// TODO: ignore order of exports, if possible/hoisted
38+
39+
return (
40+
JSON.stringify(sourceAst[0].program) ===
41+
JSON.stringify(exemplarAst[0].program)
42+
)
43+
}
44+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {
2+
AstParser,
3+
Input,
4+
NoExportError,
5+
NoMethodError,
6+
} from '@exercism/static-analysis'
7+
import { TSESTree } from '@typescript-eslint/typescript-estree'
8+
import { ExecutionOptions, WritableOutput } from '~src/interface'
9+
import {
10+
EXEMPLAR_SOLUTION,
11+
NO_METHOD,
12+
NO_NAMED_EXPORT,
13+
} from '../../../comments/shared'
14+
import { IsolatedAnalyzerImpl } from '../../IsolatedAnalyzerImpl'
15+
import { ExemplarSolution } from './ExemplarSolution'
16+
17+
type Program = TSESTree.Program
18+
19+
export class ExemplarAnalyzer extends IsolatedAnalyzerImpl {
20+
private solution!: ExemplarSolution
21+
22+
protected async execute(
23+
input: Input,
24+
output: WritableOutput,
25+
options: ExecutionOptions
26+
): Promise<void> {
27+
const [parsed] = await AstParser.ANALYZER.parse(input)
28+
29+
this.solution = this.checkStructure(parsed.program, parsed.source, output)
30+
this.solution.readExemplar(options.inputDir)
31+
32+
if (this.solution.isExemplar) {
33+
output.add(EXEMPLAR_SOLUTION())
34+
output.finish()
35+
}
36+
37+
output.finish()
38+
}
39+
40+
private checkStructure(
41+
program: Readonly<Program>,
42+
source: Readonly<string>,
43+
output: WritableOutput
44+
): ExemplarSolution | never {
45+
try {
46+
return new ExemplarSolution(program, source)
47+
} catch (error) {
48+
if (error instanceof NoMethodError) {
49+
output.add(NO_METHOD({ 'method.name': error.method }))
50+
output.finish()
51+
}
52+
53+
if (error instanceof NoExportError) {
54+
output.add(NO_NAMED_EXPORT({ 'export.name': error.namedExport }))
55+
output.finish()
56+
}
57+
58+
throw error
59+
}
60+
}
61+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { ExemplarAnalyzer } from '../__exemplar'
2+
3+
export class AnnalynsInfiltrationAnalyzer extends ExemplarAnalyzer {
4+
// TODO: implement actual analyzer
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { ExemplarAnalyzer } from '../__exemplar'
2+
3+
export class FreelancerRatesAnalyzer extends ExemplarAnalyzer {
4+
// TODO: implement actual analyzer
5+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import path from 'path'
2+
import { AnnalynsInfiltrationAnalyzer } from '~src/analyzers/concept/annalyns-infiltration'
3+
import { EXEMPLAR_SOLUTION } from '~src/comments/shared'
4+
import { DirectoryWithConfigInput } from '~src/input/DirectoryWithConfigInput'
5+
import { makeAnalyze, makeOptions } from '~test/helpers/smoke'
6+
7+
const inputDir = path.join(
8+
__dirname,
9+
'..',
10+
'..',
11+
'fixtures',
12+
'annalyns-infiltration',
13+
'exemplar'
14+
)
15+
16+
const analyze = makeAnalyze(
17+
() => new AnnalynsInfiltrationAnalyzer(),
18+
makeOptions({
19+
get inputDir(): string {
20+
return inputDir
21+
},
22+
get exercise(): string {
23+
return 'annalyns-infiltration'
24+
},
25+
})
26+
)
27+
28+
describe('When running analysis on annalyns-infiltration', () => {
29+
it('recognises the exemplar solution', async () => {
30+
const input = new DirectoryWithConfigInput(inputDir)
31+
32+
const [solution] = await input.read()
33+
const output = await analyze(solution)
34+
35+
expect(output.comments.length).toBe(1)
36+
expect(output.comments[0].type).toBe('celebratory')
37+
expect(output.comments[0].externalTemplate).toBe(
38+
EXEMPLAR_SOLUTION().externalTemplate
39+
)
40+
})
41+
})
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import path from 'path'
2+
import { FreelancerRatesAnalyzer } from '~src/analyzers/concept/freelancer-rates'
3+
import { EXEMPLAR_SOLUTION } from '~src/comments/shared'
4+
import { DirectoryWithConfigInput } from '~src/input/DirectoryWithConfigInput'
5+
import { makeAnalyze, makeOptions } from '~test/helpers/smoke'
6+
7+
const inputDir = path.join(
8+
__dirname,
9+
'..',
10+
'..',
11+
'fixtures',
12+
'freelancer-rates',
13+
'exemplar'
14+
)
15+
16+
const analyze = makeAnalyze(
17+
() => new FreelancerRatesAnalyzer(),
18+
makeOptions({
19+
get inputDir(): string {
20+
return inputDir
21+
},
22+
get exercise(): string {
23+
return 'freelancer-rates'
24+
},
25+
})
26+
)
27+
28+
describe('When running analysis on freelancer-rates', () => {
29+
it('recognises the exemplar solution', async () => {
30+
const input = new DirectoryWithConfigInput(inputDir)
31+
32+
const [solution] = await input.read()
33+
const output = await analyze(solution)
34+
35+
expect(output.comments.length).toBe(1)
36+
expect(output.comments[0].type).toBe('celebratory')
37+
expect(output.comments[0].externalTemplate).toBe(
38+
EXEMPLAR_SOLUTION().externalTemplate
39+
)
40+
})
41+
})

test/analyzers/lasagna/exemplar.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import path from 'path'
2+
import { LasagnaAnalyzer } from '~src/analyzers/concept/lasagna'
3+
import { EXEMPLAR_SOLUTION } from '~src/comments/shared'
4+
import { DirectoryWithConfigInput } from '~src/input/DirectoryWithConfigInput'
5+
import { makeAnalyze, makeOptions } from '~test/helpers/smoke'
6+
7+
const inputDir = path.join(
8+
__dirname,
9+
'..',
10+
'..',
11+
'fixtures',
12+
'lasagna',
13+
'exemplar'
14+
)
15+
16+
const analyze = makeAnalyze(
17+
() => new LasagnaAnalyzer(),
18+
makeOptions({
19+
get inputDir(): string {
20+
return inputDir
21+
},
22+
get exercise(): string {
23+
return 'lasagna'
24+
},
25+
})
26+
)
27+
28+
describe('When running analysis on lasagna', () => {
29+
it('recognises the exemplar solution', async () => {
30+
const input = new DirectoryWithConfigInput(inputDir)
31+
32+
const [solution] = await input.read()
33+
const output = await analyze(solution)
34+
35+
expect(output.comments.length).toBe(1)
36+
expect(output.comments[0].type).toBe('celebratory')
37+
expect(output.comments[0].externalTemplate).toBe(
38+
EXEMPLAR_SOLUTION().externalTemplate
39+
)
40+
})
41+
})
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"blurb": "TODO: add blurb for booleans exercise",
3+
"authors": [
4+
{
5+
"github_username": "ovidiu141",
6+
"exercism_username": "ovidiu141"
7+
}
8+
],
9+
"contributors": [
10+
{
11+
"github_username": "rishiosaur",
12+
"exercism_username": "rishiosaur"
13+
},
14+
{
15+
"github_username": "SleeplessByte",
16+
"exercism_username": "SleeplessByte"
17+
}
18+
],
19+
"files": {
20+
"solution": ["annalyns-infiltration.js"],
21+
"test": ["annalyns-infiltration.spec.js"],
22+
"exemplar": [".meta/exemplar.js"]
23+
},
24+
"forked_from": []
25+
}

0 commit comments

Comments
 (0)