Skip to content

Commit a6b4d9a

Browse files
committed
Rework into new interface and structure
Try-out towards #16
1 parent b039963 commit a6b4d9a

31 files changed

+392
-342
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"analyze:help": "yarn analyze help",
1414
"analyze:dev": "yarn build && yarn analyze",
1515
"analyze:dev:bat": "yarn build && yarn analyze:bat",
16-
"build": "yarn tsc",
16+
"build": "yarn tsc --build src",
1717
"prepublish": "yarn test",
1818
"test": "yarn build && jest"
1919
},
@@ -27,7 +27,7 @@
2727
"babel-jest": "^24.8.0",
2828
"eslint": "^5.15.3",
2929
"jest": "^24.8.0",
30-
"typescript": "^3.4.5"
30+
"typescript": "^3.5.1"
3131
},
3232
"dependencies": {
3333
"@typescript-eslint/parser": "^1.9.0",

src/analyze.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { Bootstrap } from './utils/bootstrap'
2-
import { Analyzers } from './analyzers'
3-
import { Runner } from './runner'
2+
import { find } from './analyzers/Autoload'
3+
import { run } from './runner'
44

5-
const { exercise, options, solution, logger } = Bootstrap.call()
5+
const { exercise, options, input, logger } = Bootstrap.call()
66

77
logger.log('=> DEBUG mode is on')
88
logger.log(`=> exercise: ${exercise.slug}`)
99

10-
const AnalyzerClass = Analyzers.find(exercise)
11-
const analyzer = new AnalyzerClass(solution)
10+
const AnalyzerClass = find(exercise)
11+
const analyzer = new AnalyzerClass()
1212

13-
Runner.call(analyzer, options)
13+
run(analyzer, input, options)
1414
.then(() => process.exit(0))
15-
.catch((err) => logger.fatal(err.toString()))
15+
.catch((err: any) => logger.fatal(err.toString()))
16+

src/analyzers/base_analyzer.ts renamed to src/analyzers/AnalyzerImpl.ts

Lines changed: 12 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import { parse as parseToTree, TSESTreeOptions as ParserOptions } from '@typescript-eslint/typescript-estree'
2-
import { Program } from '@typescript-eslint/typescript-estree/dist/ts-estree/ts-estree'
1+
import { getProcessLogger as getLogger, Logger } from '../utils/logger'
32

4-
import { Solution } from '../solution'
5-
import { get as getLogger, Logger } from '../utils/logger'
6-
7-
import { AnalyzerOutput } from './analyzer_output'
83
import { Comment } from '../comments/comment'
4+
import { AnalyzerOutput } from '../output/AnalyzerOutput';
5+
import { ParsedSource, AstParser } from '../parsers/AstParser';
96

107
class EarlyFinalization extends Error {
118
constructor() {
@@ -15,29 +12,15 @@ class EarlyFinalization extends Error {
1512
}
1613
}
1714

18-
export abstract class BaseAnalyzer {
15+
export abstract class AnalyzerImpl implements Analyzer {
1916
protected readonly logger: Logger
20-
protected readonly output: AnalyzerOutput
21-
22-
/**
23-
* The parser options passed to typescript-estree.parse
24-
*
25-
* @readonly
26-
* @static
27-
* @type {(ParserOptions | undefined)}
28-
*/
29-
static get parseOptions(): ParserOptions | undefined {
30-
return undefined
31-
}
17+
private output!: AnalyzerOutput
3218

3319
/**
3420
* Creates an instance of an analyzer
35-
*
36-
* @param {Solution} solution the solution
3721
*/
38-
constructor(protected readonly solution: Solution) {
22+
constructor() {
3923
this.logger = getLogger()
40-
this.output = new AnalyzerOutput()
4124
}
4225

4326
/**
@@ -50,8 +33,11 @@ export abstract class BaseAnalyzer {
5033
*
5134
* @memberof BaseAnalyzer
5235
*/
53-
public async run(): Promise<AnalyzerOutput> {
54-
await this.execute()
36+
public async run(input: Input): Promise<Output> {
37+
// Ensure each run has a fresh output
38+
this.output = new AnalyzerOutput()
39+
40+
await this.execute(input)
5541
.catch((err) => {
5642
if (err instanceof EarlyFinalization) {
5743
this.logger.log(`=> early finialization (${this.output.status})`)
@@ -126,50 +112,13 @@ export abstract class BaseAnalyzer {
126112

127113
/**
128114
* Property that returns true if there is at least one comment in the output.
129-
*
130-
* @readonly
131-
* @memberof BaseAnalyzer
132115
*/
133116
get hasCommentary() {
134117
return this.output.comments.length > 0
135118
}
136119

137120
/**
138121
* Execute the analyzer
139-
*
140-
* @protected
141-
* @abstract
142-
* @returns {Promise<void>}
143-
* @memberof BaseAnalyzer
144122
*/
145-
protected abstract execute(): Promise<void>
146-
147-
/**
148-
* Read n files from the solution
149-
*
150-
* @param solution
151-
* @param n
152-
* @returns
153-
*/
154-
protected static read(solution: Solution, n: number): Promise<Buffer[]> {
155-
return solution.read(n)
156-
}
157-
158-
/**
159-
* Parse a solution's files
160-
*
161-
* @param solution
162-
* @param n number of files expected
163-
* @returns n programs
164-
*/
165-
protected static async parse(solution: Solution, n = 1): Promise<{ program: Program, source: string }[]> {
166-
const sourceBuffers = await this.read(solution, n)
167-
const sources = sourceBuffers.map(source => source.toString())
168-
const logger = getLogger()
169-
170-
logger.log(`=> inputs: ${sources.length}`)
171-
sources.forEach(source => logger.log(`\n${source}\n`))
172-
173-
return sources.map(source => ({ program: parseToTree(source, this.parseOptions), source }))
174-
}
123+
protected abstract execute(input: Input): Promise<void>
175124
}

src/analyzers/Autoload.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Exercise } from '../exercise'
2+
3+
import path from 'path'
4+
5+
import { getProcessLogger } from '../utils/logger'
6+
7+
type AnalyzerConstructor = new () => Analyzer
8+
9+
/**
10+
* Find an analyzer for a specific exercise
11+
*
12+
* @param exercise The exericse
13+
* @returns the Analyzer constructor
14+
*/
15+
export function find(exercise: Readonly<Exercise>): AnalyzerConstructor {
16+
const file = autoload(exercise)
17+
const key = Object.keys(file).find(key => file[key] instanceof Function)
18+
19+
if (key === undefined) {
20+
throw new Error(`No Analyzer found in './${exercise.slug}`)
21+
}
22+
23+
const analyzer = file[key]
24+
getProcessLogger().log(`=> analyzer: ${analyzer.name}`)
25+
return analyzer
26+
}
27+
28+
function autoload(exercise: Readonly<Exercise>) {
29+
const modulePath = path.join(__dirname, exercise.slug, 'index') // explicit path (no extension)
30+
try {
31+
return require(modulePath)
32+
} catch(err) {
33+
const logger = getProcessLogger()
34+
logger.error(`
35+
Could not find the index.js analyzer in "${modulePath}"
36+
37+
Make sure that:
38+
- the slug "${exercise.slug}" is valid (hint: use dashes, not underscores)
39+
- there is actually an analyzer written for that exercise
40+
41+
Original error:
42+
43+
`.trimLeft())
44+
logger.fatal(JSON.stringify(err), -32)
45+
}
46+
}

src/analyzers/gigasecond/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Identifier, Node, Program, VariableDeclarator } from "@typescript-eslin
33

44
import { Traverser } from "eslint/lib/util/traverser"
55

6-
import { BaseAnalyzer } from "../base_analyzer"
6+
import { AnalyzerImpl } from "../AnalyzerImpl"
77
import { factory } from "../../comments/comment"
88

99
import { extractExport } from "../utils/extract_export"
@@ -21,6 +21,7 @@ import { isIdentifier } from "../utils/is_identifier"
2121
import { isLiteral } from "../utils/is_literal"
2222

2323
import { NO_METHOD, NO_NAMED_EXPORT, NO_PARAMETER, UNEXPECTED_PARAMETER } from "../../comments/shared";
24+
import { AstParser } from "../../parsers/AstParser";
2425

2526
const TIP_EXPORT_INLINE = factory<'method_signature' | 'const_name'>`
2627
Did you know that you can export functions, classes and constants directly
@@ -48,7 +49,9 @@ export const gigasecond = (...)
4849
`('javascript.gigasecond.prefer_top_level_constant')
4950

5051

51-
export class GigasecondAnalyzer extends BaseAnalyzer {
52+
export class GigasecondAnalyzer extends AnalyzerImpl {
53+
54+
static Parser: AstParser = new AstParser(undefined, 1)
5255

5356
private program!: Program
5457
private source!: string
@@ -83,8 +86,8 @@ export class GigasecondAnalyzer extends BaseAnalyzer {
8386
return this._mainParameter
8487
}
8588

86-
public async execute(): Promise<void> {
87-
const [parsed] = await GigasecondAnalyzer.parse(this.solution)
89+
public async execute(input: Input): Promise<void> {
90+
const [parsed] = await GigasecondAnalyzer.Parser.parse(input)
8891

8992
this.program = parsed.program
9093
this.source = parsed.source

src/analyzers/index.ts

Lines changed: 0 additions & 52 deletions
This file was deleted.

src/analyzers/two-fer/index.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from "@typescript-eslint/typescript-estree/dist/ts-estree/ts-estree"
1010
import { AST_NODE_TYPES } from "@typescript-eslint/typescript-estree"
1111

12-
import { BaseAnalyzer } from "../base_analyzer"
12+
import { AnalyzerImpl } from "../AnalyzerImpl"
1313

1414
import { extractAll } from "../utils/extract_all"
1515
import { extractExport } from "../utils/extract_export"
@@ -35,6 +35,7 @@ import { isLiteral } from "../utils/is_literal";
3535
import { isTemplateLiteral } from "../utils/is_template_literal";
3636
import { isUnaryExpression } from "../utils/is_unary_expression";
3737
import { isLogicalExpression } from "../utils/is_logical_expression";
38+
import { AstParser } from "../../parsers/AstParser";
3839

3940
const OPTIMISE_DEFAULT_VALUE = factory<'parameter'>`
4041
You currently use a conditional to branch in case there is no value passed into
@@ -57,7 +58,11 @@ Did you know that you can export functions, classes and constants directly
5758
inline?
5859
`('javascript.two-fer.export_inline')
5960

60-
export class TwoFerAnalyzer extends BaseAnalyzer {
61+
const Parser: AstParser = new AstParser(undefined, 1)
62+
63+
64+
export class TwoFerAnalyzer extends AnalyzerImpl {
65+
6166

6267
private program!: Program
6368
private source!: string
@@ -89,8 +94,8 @@ export class TwoFerAnalyzer extends BaseAnalyzer {
8994
return this._mainParameter
9095
}
9196

92-
public async execute(): Promise<void> {
93-
const [parsed] = await TwoFerAnalyzer.parse(this.solution)
97+
public async execute(input: Input): Promise<void> {
98+
const [parsed] = await Parser.parse(input)
9499

95100
this.program = parsed.program
96101
this.source = parsed.source
File renamed without changes.

src/input/DirectoryInput.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { readDir } from "../utils/fs";
2+
import { FileInput } from "./FileInput";
3+
4+
import path from 'path'
5+
6+
const EXTENSIONS = /\.(jsx?|tsx?|mjs)$/
7+
const TEST_FILES = /\.spec|test\./
8+
const CONFIGURATION_FILES = /(?:babel\.config\.js|jest\.config\.js|\.eslintrc\.js)$/
9+
10+
export class DirectoryInput implements Input {
11+
constructor(private readonly path: string, private readonly exerciseSlug: string) {}
12+
13+
async read(n = 1): Promise<string[]> {
14+
const files = await readDir(this.path)
15+
16+
const candidates = findCandidates(files, n, `${this.exerciseSlug}.js`)
17+
const fileSources = await Promise.all(
18+
candidates.map(candidate => new FileInput(path.join(this.path, candidate)).read().then(([source]) => source))
19+
)
20+
21+
return fileSources
22+
}
23+
}
24+
25+
/**
26+
* Given a list of files, finds up to n files that are not test files and have
27+
* an extension that will probably work with the estree analyzer.
28+
*
29+
* @param files the file candidates
30+
* @param n the number of files it should return
31+
* @param preferredNames the names of the files it prefers
32+
*/
33+
function findCandidates(files: string[], n: number, ...preferredNames: string[]) {
34+
const candidates = files
35+
.filter(file => EXTENSIONS.test(file))
36+
.filter(file => !TEST_FILES.test(file))
37+
.filter(file => !CONFIGURATION_FILES.test(file))
38+
39+
const preferredMatches = preferredNames
40+
? candidates.filter(file => preferredNames.includes(file))
41+
: []
42+
43+
const allMatches = preferredMatches.length >= n
44+
? preferredMatches
45+
: preferredMatches.concat(candidates.filter(file => !preferredMatches.includes(file)))
46+
47+
return allMatches.slice(0, n)
48+
}

src/input/FileInput.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { readFile } from "../utils/fs";
2+
3+
export class FileInput implements Input {
4+
constructor(private readonly path: string) {}
5+
6+
async read(n = 1): Promise<string[]> {
7+
const buffer = await readFile(this.path)
8+
return [buffer.toString("utf8")]
9+
}
10+
}

0 commit comments

Comments
 (0)