Skip to content

Commit

Permalink
Rework into new interface and structure
Browse files Browse the repository at this point in the history
Try-out towards #16
  • Loading branch information
SleeplessByte committed Jun 4, 2019
1 parent 86324e4 commit 88dfcac
Show file tree
Hide file tree
Showing 31 changed files with 392 additions and 342 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"analyze:help": "yarn analyze help",
"analyze:dev": "yarn build && yarn analyze",
"analyze:dev:bat": "yarn build && yarn analyze:bat",
"build": "yarn tsc",
"build": "yarn tsc --build src",
"prepublish": "yarn test",
"test": "yarn build && jest"
},
Expand All @@ -27,7 +27,7 @@
"babel-jest": "^24.8.0",
"eslint": "^5.15.3",
"jest": "^24.8.0",
"typescript": "^3.4.5"
"typescript": "^3.5.1"
},
"dependencies": {
"@typescript-eslint/parser": "^1.9.0",
Expand Down
15 changes: 8 additions & 7 deletions src/analyze.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Bootstrap } from './utils/bootstrap'
import { Analyzers } from './analyzers'
import { Runner } from './runner'
import { find } from './analyzers/Autoload'
import { run } from './runner'

const { exercise, options, solution, logger } = Bootstrap.call()
const { exercise, options, input, logger } = Bootstrap.call()

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

const AnalyzerClass = Analyzers.find(exercise)
const analyzer = new AnalyzerClass(solution)
const AnalyzerClass = find(exercise)
const analyzer = new AnalyzerClass()

Runner.call(analyzer, options)
run(analyzer, input, options)
.then(() => process.exit(0))
.catch((err) => logger.fatal(err.toString()))
.catch((err: any) => logger.fatal(err.toString()))

75 changes: 12 additions & 63 deletions src/analyzers/base_analyzer.ts → src/analyzers/AnalyzerImpl.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { parse as parseToTree, TSESTreeOptions as ParserOptions } from '@typescript-eslint/typescript-estree'
import { Program } from '@typescript-eslint/typescript-estree/dist/ts-estree/ts-estree'
import { getProcessLogger as getLogger, Logger } from '../utils/logger'

import { Solution } from '../solution'
import { get as getLogger, Logger } from '../utils/logger'

import { AnalyzerOutput } from './analyzer_output'
import { Comment } from '../comments/comment'
import { AnalyzerOutput } from '../output/AnalyzerOutput';
import { ParsedSource, AstParser } from '../parsers/AstParser';

class EarlyFinalization extends Error {
constructor() {
Expand All @@ -15,29 +12,15 @@ class EarlyFinalization extends Error {
}
}

export abstract class BaseAnalyzer {
export abstract class AnalyzerImpl implements Analyzer {
protected readonly logger: Logger
protected readonly output: AnalyzerOutput

/**
* The parser options passed to typescript-estree.parse
*
* @readonly
* @static
* @type {(ParserOptions | undefined)}
*/
static get parseOptions(): ParserOptions | undefined {
return undefined
}
private output!: AnalyzerOutput

/**
* Creates an instance of an analyzer
*
* @param {Solution} solution the solution
*/
constructor(protected readonly solution: Solution) {
constructor() {
this.logger = getLogger()
this.output = new AnalyzerOutput()
}

/**
Expand All @@ -50,8 +33,11 @@ export abstract class BaseAnalyzer {
*
* @memberof BaseAnalyzer
*/
public async run(): Promise<AnalyzerOutput> {
await this.execute()
public async run(input: Input): Promise<Output> {
// Ensure each run has a fresh output
this.output = new AnalyzerOutput()

await this.execute(input)
.catch((err) => {
if (err instanceof EarlyFinalization) {
this.logger.log(`=> early finialization (${this.output.status})`)
Expand Down Expand Up @@ -126,50 +112,13 @@ export abstract class BaseAnalyzer {

/**
* Property that returns true if there is at least one comment in the output.
*
* @readonly
* @memberof BaseAnalyzer
*/
get hasCommentary() {
return this.output.comments.length > 0
}

/**
* Execute the analyzer
*
* @protected
* @abstract
* @returns {Promise<void>}
* @memberof BaseAnalyzer
*/
protected abstract execute(): Promise<void>

/**
* Read n files from the solution
*
* @param solution
* @param n
* @returns
*/
protected static read(solution: Solution, n: number): Promise<Buffer[]> {
return solution.read(n)
}

/**
* Parse a solution's files
*
* @param solution
* @param n number of files expected
* @returns n programs
*/
protected static async parse(solution: Solution, n = 1): Promise<{ program: Program, source: string }[]> {
const sourceBuffers = await this.read(solution, n)
const sources = sourceBuffers.map(source => source.toString())
const logger = getLogger()

logger.log(`=> inputs: ${sources.length}`)
sources.forEach(source => logger.log(`\n${source}\n`))

return sources.map(source => ({ program: parseToTree(source, this.parseOptions), source }))
}
protected abstract execute(input: Input): Promise<void>
}
46 changes: 46 additions & 0 deletions src/analyzers/Autoload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Exercise } from '../exercise'

import path from 'path'

import { getProcessLogger } from '../utils/logger'

type AnalyzerConstructor = new () => Analyzer

/**
* Find an analyzer for a specific exercise
*
* @param exercise The exericse
* @returns the Analyzer constructor
*/
export function find(exercise: Readonly<Exercise>): AnalyzerConstructor {
const file = autoload(exercise)
const key = Object.keys(file).find(key => file[key] instanceof Function)

if (key === undefined) {
throw new Error(`No Analyzer found in './${exercise.slug}`)
}

const analyzer = file[key]
getProcessLogger().log(`=> analyzer: ${analyzer.name}`)
return analyzer
}

function autoload(exercise: Readonly<Exercise>) {
const modulePath = path.join(__dirname, exercise.slug, 'index') // explicit path (no extension)
try {
return require(modulePath)
} catch(err) {
const logger = getProcessLogger()
logger.error(`
Could not find the index.js analyzer in "${modulePath}"
Make sure that:
- the slug "${exercise.slug}" is valid (hint: use dashes, not underscores)
- there is actually an analyzer written for that exercise
Original error:
`.trimLeft())
logger.fatal(JSON.stringify(err), -32)
}
}
11 changes: 7 additions & 4 deletions src/analyzers/gigasecond/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Identifier, Node, Program, VariableDeclarator } from "@typescript-eslin

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

import { BaseAnalyzer } from "../base_analyzer"
import { AnalyzerImpl } from "../AnalyzerImpl"
import { factory } from "../../comments/comment"

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

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

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


export class GigasecondAnalyzer extends BaseAnalyzer {
export class GigasecondAnalyzer extends AnalyzerImpl {

static Parser: AstParser = new AstParser(undefined, 1)

private program!: Program
private source!: string
Expand Down Expand Up @@ -83,8 +86,8 @@ export class GigasecondAnalyzer extends BaseAnalyzer {
return this._mainParameter
}

public async execute(): Promise<void> {
const [parsed] = await GigasecondAnalyzer.parse(this.solution)
public async execute(input: Input): Promise<void> {
const [parsed] = await GigasecondAnalyzer.Parser.parse(input)

this.program = parsed.program
this.source = parsed.source
Expand Down
52 changes: 0 additions & 52 deletions src/analyzers/index.ts

This file was deleted.

13 changes: 9 additions & 4 deletions src/analyzers/two-fer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "@typescript-eslint/typescript-estree/dist/ts-estree/ts-estree"
import { AST_NODE_TYPES } from "@typescript-eslint/typescript-estree"

import { BaseAnalyzer } from "../base_analyzer"
import { AnalyzerImpl } from "../AnalyzerImpl"

import { extractAll } from "../utils/extract_all"
import { extractExport } from "../utils/extract_export"
Expand All @@ -35,6 +35,7 @@ import { isLiteral } from "../utils/is_literal";
import { isTemplateLiteral } from "../utils/is_template_literal";
import { isUnaryExpression } from "../utils/is_unary_expression";
import { isLogicalExpression } from "../utils/is_logical_expression";
import { AstParser } from "../../parsers/AstParser";

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

export class TwoFerAnalyzer extends BaseAnalyzer {
const Parser: AstParser = new AstParser(undefined, 1)


export class TwoFerAnalyzer extends AnalyzerImpl {


private program!: Program
private source!: string
Expand Down Expand Up @@ -89,8 +94,8 @@ export class TwoFerAnalyzer extends BaseAnalyzer {
return this._mainParameter
}

public async execute(): Promise<void> {
const [parsed] = await TwoFerAnalyzer.parse(this.solution)
public async execute(input: Input): Promise<void> {
const [parsed] = await Parser.parse(input)

this.program = parsed.program
this.source = parsed.source
Expand Down
File renamed without changes.
48 changes: 48 additions & 0 deletions src/input/DirectoryInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { readDir } from "../utils/fs";
import { FileInput } from "./FileInput";

import path from 'path'

const EXTENSIONS = /\.(jsx?|tsx?|mjs)$/
const TEST_FILES = /\.spec|test\./
const CONFIGURATION_FILES = /(?:babel\.config\.js|jest\.config\.js|\.eslintrc\.js)$/

export class DirectoryInput implements Input {
constructor(private readonly path: string, private readonly exerciseSlug: string) {}

async read(n = 1): Promise<string[]> {
const files = await readDir(this.path)

const candidates = findCandidates(files, n, `${this.exerciseSlug}.js`)
const fileSources = await Promise.all(
candidates.map(candidate => new FileInput(path.join(this.path, candidate)).read().then(([source]) => source))
)

return fileSources
}
}

/**
* Given a list of files, finds up to n files that are not test files and have
* an extension that will probably work with the estree analyzer.
*
* @param files the file candidates
* @param n the number of files it should return
* @param preferredNames the names of the files it prefers
*/
function findCandidates(files: string[], n: number, ...preferredNames: string[]) {
const candidates = files
.filter(file => EXTENSIONS.test(file))
.filter(file => !TEST_FILES.test(file))
.filter(file => !CONFIGURATION_FILES.test(file))

const preferredMatches = preferredNames
? candidates.filter(file => preferredNames.includes(file))
: []

const allMatches = preferredMatches.length >= n
? preferredMatches
: preferredMatches.concat(candidates.filter(file => !preferredMatches.includes(file)))

return allMatches.slice(0, n)
}
10 changes: 10 additions & 0 deletions src/input/FileInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { readFile } from "../utils/fs";

export class FileInput implements Input {
constructor(private readonly path: string) {}

async read(n = 1): Promise<string[]> {
const buffer = await readFile(this.path)
return [buffer.toString("utf8")]
}
}
Loading

0 comments on commit 88dfcac

Please sign in to comment.