Skip to content

Design and apply a proper interface and structure #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 4, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}
2 changes: 1 addition & 1 deletion bin/analyze.bat
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@REM Usage:
@REM ./bin/analyze.bat two_fer ~/test/
@REM ./bin/analyze.bat two-fer ~/test/

node -r esm ./dist/analyze.js %*
2 changes: 1 addition & 1 deletion bin/analyze.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env sh

# Usage:
# ./bin/analyze.sh two_fer ~/test/
# ./bin/analyze.sh two-fer ~/folder/to/solution

node -r esm ./dist/analyze.js "$@"
166 changes: 0 additions & 166 deletions bin/batch-runner

This file was deleted.

4 changes: 4 additions & 0 deletions bin/batch.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@REM Usage:
@REM ./bin/batch.bat two-fer

node -r esm ./dist/batch.js %*
6 changes: 6 additions & 0 deletions bin/batch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env sh

# Usage:
# ./bin/batch.sh two-fer

node -r esm ./dist/batch.js "$@"
85 changes: 0 additions & 85 deletions bin/statistics

This file was deleted.

4 changes: 4 additions & 0 deletions bin/stats.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@REM Usage:
@REM ./bin/stats.bat two-fer

node -r esm ./dist/stats.js %*
6 changes: 6 additions & 0 deletions bin/stats.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env sh

# Usage:
# ./bin/stats.sh two-fer

node -r esm ./dist/stats.js "$@"
Empty file added docs/.keep
Empty file.
28 changes: 16 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
{
"name": "exercism-javascript-analyzer",
"name": "@exercism/javascript-analyzer",
"version": "0.1.0",
"description": "Exercism analyzer for javascript",
"main": "index.js",
"repository": "https://github.com/exercism/javascript-analyzer",
"author": "Derk-Jan Karrenbeld <[email protected]>",
"license": "MIT",
"bin": "./bin/analyze.sh",
"directories": {
"lib": "./dist",
"bin": "./bin",
"doc": "./docs",
"test": "./test"
},
"scripts": {
"analyze": "bin/analyze.sh",
"analyze:bat": "bin/analyze.bat",
"analyze": "./bin/analyze.sh",
"analyze:bat": "./bin/analyze.bat",
"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"
},
@@ -22,17 +26,17 @@
"@babel/preset-env": "^7.4.5",
"@babel/preset-typescript": "^7.3.3",
"@types/jest": "^24.0.13",
"@types/node": "^11.11.6",
"@types/yargs": "^12.0.10",
"@types/node": "^12.0.4",
"@types/yargs": "^13.0.0",
"babel-jest": "^24.8.0",
"eslint": "^5.15.3",
"eslint": "^5.16.0",
"jest": "^24.8.0",
"typescript": "^3.4.5"
"typescript": "^3.5.1"
},
"dependencies": {
"@typescript-eslint/parser": "^1.9.0",
"@typescript-eslint/typescript-estree": "^1.9.0",
"esm": "^3.2.20",
"yargs": "^13.2.2"
"esm": "^3.2.25",
"yargs": "^13.2.4"
}
}
2 changes: 1 addition & 1 deletion src/exercise.ts → src/ExerciseImpl.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export class Exercise {
export class ExerciseImpl implements Exercise {
constructor(public readonly slug: string) {
if (!slug) {
throw new Error(`Expected valid exercise slug, got '${slug}'`)
36 changes: 29 additions & 7 deletions src/analyze.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
import { Bootstrap } from './utils/bootstrap'
import { Analyzers } from './analyzers'
import { Runner } from './runner'
import { find } from './analyzers/Autoload'
import { run } from './utils/runner'

const { exercise, options, solution, logger } = Bootstrap.call()
// The bootstrap call uses the arguments passed to the process to figure out
// which exercise to target, where the input lives (directory input) and what
// execution options to set.
//
// analyze -dc two-fer ~/test/
//
// For example, if arguments are passed directly, the above will run the two-fer
// exercise analyzer with the ~/test/ input directory and turning on debug and
// console logging.
//
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)
// The autoloader knows where an analyzer should live and tries to require it
// so it can be instantiated here. This allows us to add new analyzers without
// needing to update a bookkeeping construct
//
const AnalyzerClass = find(exercise)
const analyzer = new AnalyzerClass()

Runner.call(analyzer, options)
// The runner uses the execution options to determine what should happen with
// the output. For example the --dry flag will make sure there is nothing
// written to a file.
//
// The basis for the runner is calling analyzer.run(input) -- the output is then
// logged and/or written to a file.
//
run(analyzer, input, options)
.then(() => process.exit(0))
.catch((err) => logger.fatal(err.toString()))
.catch((err: any) => logger.fatal(err.toString()))

85 changes: 21 additions & 64 deletions src/analyzers/base_analyzer.ts → src/analyzers/AnalyzerImpl.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
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() {
@@ -15,29 +11,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()
}

/**
@@ -50,8 +32,20 @@ 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
//
// Note: still need to wait for a run to complete before the next one can be
// started. We could work around this by providing an execution
// context that is fresh on each run.
//
// The reason output is not passed to execute, is that it doesn't _actually_
// enforce the implementing analyzer to not use local state, so we don't
// gain anything by it.
//
this.output = new AnalyzerOutput()

await this.execute(input)
.catch((err) => {
if (err instanceof EarlyFinalization) {
this.logger.log(`=> early finialization (${this.output.status})`)
@@ -126,50 +120,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>
}
44 changes: 44 additions & 0 deletions src/analyzers/Autoload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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
@@ -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"
@@ -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
@@ -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
@@ -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
52 changes: 0 additions & 52 deletions src/analyzers/index.ts

This file was deleted.

16 changes: 10 additions & 6 deletions src/analyzers/two-fer/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
BinaryExpression,
ConditionalExpression,
IfStatement,
LogicalExpression,
@@ -9,7 +8,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"
@@ -35,6 +34,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
@@ -57,7 +57,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
@@ -89,8 +93,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
@@ -241,7 +245,7 @@ export class TwoFerAnalyzer extends BaseAnalyzer {
}

private checkForSolutionWithoutStringTemplate() {
const [expression] = extractAll<BinaryExpression>(this.mainMethod!, AST_NODE_TYPES.BinaryExpression)
const [expression] = extractAll(this.mainMethod!, AST_NODE_TYPES.BinaryExpression)


//
200 changes: 200 additions & 0 deletions src/batch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@

import fs from 'fs'
import path from 'path'

import { Bootstrap } from './utils/bootstrap'
import { find } from './analyzers/Autoload'
import { readDir } from './utils/fs';
import { DirectoryInput } from './input/DirectoryInput'
import { FileOutput } from './output/processor/FileOutput';

// The bootstrap call uses the arguments passed to the process to figure out
// which exercise to target, where the input lives (directory input) and what
// execution options to set.
//
// batch -c two-fer ~/fixtures
//
// For example, if arguments are passed directly, the above will run the two-fer
// exercise analyzer for all the folders inside the two-fer fixture folder, with
// console log output turned on
//
const { exercise, options, logger } = Bootstrap.call()

const AnalyzerClass = find(exercise)
const FIXTURES_ROOT = path.join(options.inputDir || path.join(__dirname, '..', 'test', 'fixtures'), exercise.slug)

/**
* Pad the input `value` to `length` using the `padc` pad character
*
* @param {(string | number | bigint)} value
* @param {number} [length=20]
* @param {string} [padc=' ']
* @returns {string} the padded string
*/
function pad(value: string | number | bigint, length: number = 20, padc: string = ' '): string {
const pad = Array(length).fill(padc).join('')
return (pad + value).slice(-length)
}

const DEFAULT_LINE_DATA = {
count: 0,
comments: {
unique: [] as string[],
unique_templates: [] as string[]
},
runtimes: {
total: BigInt(0),
average: BigInt(0),
median: BigInt(0)
}
}

/**
* Turns a data set into a table row
*
* @param {string} humanStatus
* @param {typeof DEFAULT_LINE_DATA} [data=DEFAULT_LINE_DATA]
* @returns {string}
*/
function line(humanStatus: string, data = DEFAULT_LINE_DATA): string {
const {
count,
comments: { unique, unique_templates },
runtimes: { total, average, median }
} = data
return `| ${[
pad(humanStatus, 20),
pad(count, 5),
pad(unique.length, 8),
pad(unique_templates.length, 6),
`${pad((average / BigInt(1000000)), 4)}ms`,
`${pad((median / BigInt(1000000)), 4)}ms`,
`${pad((total / BigInt(10000000000)), 6)}s`
].join(' | ')
} |`
}

const rootTimeStamp = process.hrtime.bigint()
logger.log(`=> start batch runner for ${exercise.slug}`)

readDir(FIXTURES_ROOT)
.then(async (fixtureDirs) => Promise.all(fixtureDirs.map(async (fixtureDir) => {
try {
const inputDir = path.join(FIXTURES_ROOT, fixtureDir)
const input = new DirectoryInput(inputDir, exercise.slug)
const analyzer = new AnalyzerClass()

const fixtureStamp = process.hrtime.bigint()
const analysis = await analyzer.run(input)
const runtime = process.hrtime.bigint() - fixtureStamp

await FileOutput(analysis.toProcessable(options), { ...options, inputDir, output: './analysis.json' })

return { result: analysis, runtime }
} catch (_ignore) {
return undefined
}
}))
)
.then((results) => results.filter(Boolean) as ReadonlyArray<{ result: Output, runtime: bigint }>)
.then((results) => {
return results.reduce((groups, { result: { status, comments }, runtime }) => {
groups[status] = (groups[status] || { runtimes: [], comments: [], count: 0 })

groups[status].runtimes.push(runtime)
groups[status].comments.push(...comments)
groups[status].count += 1

return groups
}, {} as { [K in Output['status']]: { runtimes: bigint[], count: number, comments: Comment[] } })
})
.then((grouped) => {
const aggregatedGroups = (Object.keys(grouped) as Output['status'][]).reduce((aggregated, status) => {
const { count, comments, runtimes } = grouped[status]

const sortedRuntimes = runtimes.sort()

const totalRuntime = runtimes.reduce((result, time) => result + time, BigInt(0))
const averageRuntime = totalRuntime / BigInt(sortedRuntimes.length)
const medianRuntime = sortedRuntimes[(sortedRuntimes.length / 2) | 0]

const uniqueComments = [...new Set(comments.filter(Boolean).map(comment => comment.message))]
const uniqueTemplates = [...new Set(comments.filter(Boolean).map(comment => comment.template))]

return { ...aggregated,
[status]: {
count,
comments: {
unique: uniqueComments,
unique_templates: uniqueTemplates
},
runtimes: {
total: totalRuntime,
average: averageRuntime,
median: medianRuntime
}
}
}
}, {} as { [K in Output['status']]: { count: number, comments: { unique: string[], unique_templates: string[] }, runtimes: { total: bigint, average: bigint, median: bigint }}})

const groupKeys = (Object.keys(aggregatedGroups) as Output['status'][])
const allRuntimesSorted = groupKeys.reduce((runtimes, status) => runtimes.concat(grouped[status].runtimes), [] as bigint[]).sort()

const totalCount = groupKeys.reduce((result, status) => result + aggregatedGroups[status].count, 0)
const totalRuntime = groupKeys.reduce((result, status) => result + aggregatedGroups[status].runtimes.total, BigInt(0))
const totalAverageRuntime = totalRuntime / BigInt(allRuntimesSorted.length)
const totalMedianRuntime = allRuntimesSorted[(allRuntimesSorted.length / 2) | 0]
const totalTotalRuntime = groupKeys.reduce((result, status) => result + aggregatedGroups[status].runtimes.total, BigInt(0))

const allComments = groupKeys.reduce((comments, status) => comments.concat(grouped[status].comments), [] as Comment[])
const allUniqueComments = [...new Set(allComments.filter(Boolean).map(comment => comment.message))]
const allUniqueTemplates = [...new Set(allComments.filter(Boolean).map(comment => comment.template))]

const totalData = {
count: totalCount,
comments: {
unique: allUniqueComments,
unique_templates: allUniqueTemplates
},
runtimes: {
total: totalTotalRuntime,
average: totalAverageRuntime,
median: totalMedianRuntime
}
}

process.stdout.write(`
## Raw
\`\`\`json
${JSON.stringify(groupKeys.reduce((serializable, status) => {
return {
...serializable,
[status]: {
...aggregatedGroups[status],
runtimes: {
total: Number(aggregatedGroups[status].runtimes.total.toString()),
average: Number(aggregatedGroups[status].runtimes.average.toString()),
median: Number(aggregatedGroups[status].runtimes.median.toString())
}
}
}
}, {
toolRuntime: ((process.hrtime.bigint() - rootTimeStamp) / BigInt(1000000)).toString() + 'ms'
}), null, 2)}
\`\`\``)


process.stdout.write('\n\n')
process.stdout.write(`
## Statistics
| Status | Count | Comments | Unique | Avg | Median | Total |
| --------------------:| -----:| --------:| ------:| ------:|-------:|--------:|
${line('Approve (optimal)', aggregatedGroups['approve_as_optimal'])}
${line('Approve (comment)', aggregatedGroups['approve_with_comment'])}
${line('Disapprove (comment)', aggregatedGroups['disapprove_with_comment'])}
${line( 'Refer to mentor', aggregatedGroups['refer_to_mentor'])}
${line('Total', totalData)}
`.trim())
})
4 changes: 2 additions & 2 deletions src/comments/comment.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ type TemplateKeys = Array<number | string>

export type FactoryResultParameter<R extends string = string> = Array<string> | [Record<R, string | undefined>]

export class Comment {
export class CommentImpl implements Comment {

constructor(
public readonly message: string,
@@ -65,7 +65,7 @@ export function factory<R extends string = ''>(strings: TemplateStringsArray, ..
template += tag + next
}

return new Comment(
return new CommentImpl(
message.trimRight(),
template.trimRight(),
dict as any,
File renamed without changes.
1 change: 1 addition & 0 deletions src/errors/codes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const GENERIC_FAILURE = -1
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")]
}
}
74 changes: 74 additions & 0 deletions src/interface.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
interface ExecutionOptions {
/** If true, logger.debug messages are displayed */
debug: boolean
/** If true, logger messages are sent to the console */
console: boolean
/** If true, does a dry run and does not output anything to file */
dry: boolean
/** The output file name */
output: string
/** The input directory path */
inputDir: string
/** The exercise slug */
exercise: string
/** If true, expects website-copy to provide the contents of the templates */
templates: boolean
}

interface AstParser<T extends object> {
/**
* Parse an input to an Abstract Syntax Tree
* @param input the input
* @returns the AST
*/
parse(input: Input): Promise<T>
}

interface Input {
/**
* Read in a number of strings
* @param n the number
* @returns at most `n` strings
*/
read(n?: number): Promise<string[]>
}


interface Exercise {
readonly slug: string
}

interface Comment {
/** The constructed message with all the template variables applied */
message: string
/** The message with the template variables in there */
template: string
/** The provided variables as array or name (key), value (value) map */
variables: Readonly<{ [name: string]: string | undefined, [name: number]: string | undefined }>
/** The identifier for the template on website-copy */
externalTemplate: string
}

interface Output {
status: 'refer_to_mentor' | 'approve_as_optimal' | 'approve_with_comment' | 'disapprove_with_comment'
comments: Comment[]

/**
* Makes the output ready to be processed
* @param options the execution options
* @returns the output as string
*/
toProcessable(options: Readonly<ExecutionOptions>): Promise<string>
}

interface OutputProcessor {
(previous: Promise<string>, options: Readonly<ExecutionOptions>): Promise<string>
}

interface Analyzer {
run(input: Input): Promise<Output>
}

interface Runner {
call(analyzer: Analyzer, input: Input, options: Readonly<ExecutionOptions>): Promise<Output>
}
61 changes: 29 additions & 32 deletions src/analyzers/analyzer_output.ts → src/output/AnalyzerOutput.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
import fs from 'fs'

import { Comment } from '../comments/comment'
import { ExecutionOptions } from '../utils/execution_options';

enum SolutionStatus {
/** This is the default situation and should be used when there is any uncertainty. */
Redirect = 'refer_to_mentor',
@@ -22,7 +17,7 @@ enum SolutionStatus {
* @export
* @class AnalyzerOutput
*/
export class AnalyzerOutput {
export class AnalyzerOutput implements Output {
public status: SolutionStatus
public comments: Comment[]

@@ -73,36 +68,38 @@ export class AnalyzerOutput {
* [doc]: https://github.com/exercism/automated-mentoring-support/blob/master/docs/interface.md#output-format
*
* @param {ExecutionOptions} options
* @returns {string}
* @returns {Promise<string>}
*/
public toString(options: ExecutionOptions): string {
return JSON.stringify({
status: this.status,
comments: this.comments.map((comment) => {
if (!comment.variables || Object.keys(comment.variables).length === 0) {
return options.templates ? comment.externalTemplate : comment.message
}
toProcessable({ templates }: ExecutionOptions): Promise<string> {
return Promise.resolve(
JSON.stringify({
status: this.status,
comments: this.comments.map(templates ? makeExternalComment : makeIsolatedComment)

return {
comment: options.templates ? comment.externalTemplate : comment.template,
params: comment.variables
}
})
}, null, 2)
)
}
}

/**
* Writes self to `path`
*
* @param {string} path
* @param {ExecutionOptions} options
* @returns {Promise<void>} The future which resolves / rejects when its done
*/
public writeTo(path: string, options: ExecutionOptions): Promise<void> {
return new Promise((resolve, reject) => {
fs.writeFile(path, this.toString(options), (err) => {
err ? reject(err) : resolve()
})
})
function makeExternalComment(comment: Comment) {
if (!comment.variables || Object.keys(comment.variables).length === 0) {
return comment.externalTemplate
}

return {
comment: comment.externalTemplate,
params: comment.variables
}
}

function makeIsolatedComment(comment: Comment) {
if (!comment.variables || Object.keys(comment.variables).length === 0) {
return comment.message
}

return {
comment: comment.template,
params: comment.variables
}
}

24 changes: 24 additions & 0 deletions src/output/processor/FileOutput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import fs from 'fs'
import path from 'path'

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

type FileOutputOptions = Pick<ExecutionOptions, 'output' | 'inputDir'>

export const FileOutput: OutputProcessor = async (previous: Promise<string>, options: FileOutputOptions): Promise<string> => {
const output = await previous
const outputPath = getOutputPath(options)
getProcessLogger().log(`=> writing output to ${outputPath}`)

return new Promise((resolve, reject) => {
fs.writeFile(outputPath, output, (err) => {
err ? reject(err) : resolve(output)
})
})
}

function getOutputPath({ output, inputDir }: FileOutputOptions): string {
return path.isAbsolute(output)
? output
: path.join(inputDir, output)
}
7 changes: 7 additions & 0 deletions src/output/processor/LogOutput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { getProcessLogger } from '../../utils/logger'

export const LogOutput: OutputProcessor = async (previous: Promise<string>): Promise<string> => {
const output = await previous
getProcessLogger().log(`=> output: \n\n${output}\n`)
return output
}
4 changes: 4 additions & 0 deletions src/output/processor/PassThroughOutput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

export const PassThroughOutput: OutputProcessor = async (previous: Promise<string>): Promise<string> => {
return previous
}
30 changes: 30 additions & 0 deletions src/parsers/AstParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { parse as parseToTree, TSESTree, TSESTreeOptions } from "@typescript-eslint/typescript-estree";
import { getProcessLogger } from "../utils/logger";

type Program = TSESTree.Program

export class AstParser {
public constructor(private readonly options?: TSESTreeOptions, private readonly n = 1) {
}

/**
* Parse a files into an AST tree
*
* @param solution
* @returns n programs
*/
async parse(input: Input): Promise<{ program: Program, source: string }[]> {
const sources = await input.read(this.n)

const logger = getProcessLogger()

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

return sources.map(source => new ParsedSource(parseToTree(source, this.options), source))
}
}

export class ParsedSource {
public constructor(public readonly program: Program, public readonly source: string) {}
}
53 changes: 0 additions & 53 deletions src/runner.ts

This file was deleted.

64 changes: 0 additions & 64 deletions src/solution.ts

This file was deleted.

104 changes: 104 additions & 0 deletions src/stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@

import path from 'path'

import { Bootstrap } from './utils/bootstrap'
import { readDir } from './utils/fs';
import { DirectoryInput } from './input/DirectoryInput'
import { AstParser } from './parsers/AstParser';
import { Node } from '@typescript-eslint/typescript-estree/dist/ts-estree/ts-estree';

// The bootstrap call uses the arguments passed to the process to figure out
// which exercise to target, where the input lives (directory input) and what
// execution options to set.
//
// stats -c two-fer ~/fixtures
//
// For example, if arguments are passed directly, the above will run the two-fer
// exercise analyzer (dry: without output) for all the folders inside the
// two-fer fixture folder, with console log output turned on
//
const { exercise, options, logger } = Bootstrap.call()

const FIXTURES_ROOT = path.join(options.inputDir || path.join(__dirname, '..', 'test', 'fixtures'), exercise.slug)

function pad(value: string | number, pad = ' ') {
return (pad + value).slice(-pad.length)
}

logger.log(`=> start statistics collection for ${exercise.slug}`)

const parser = new AstParser({ comment: false, tokens: false, loc: false, range: false })

readDir(FIXTURES_ROOT)
.then(async (fixtureDirs) => Promise.all(fixtureDirs.map(async (fixtureDir) => {
const inputDirectory = path.join(FIXTURES_ROOT, fixtureDir)
try {
const input = new DirectoryInput(inputDirectory, exercise.slug)
const results = await parser.parse(input)

if (results.length === 0) {
throw new Error(`No input source files for ${exercise.slug}`)
}

const [{ program: root }] = results

// There can be a bug where loc and range data is not removed
if (root.loc || root.range) {
delete root.comments
delete root.tokens
delete root.loc
delete root.range

require('eslint/lib/util/traverser').traverse(
root, {
enter(node: Node) {
delete node.loc
delete node.range
},

// Use typescript visitor keys (otherwise type annotations are not removed)
visitorKeys: require("@typescript-eslint/parser/dist/visitor-keys").visitorKeys
}
)
}

return JSON.stringify(root)
} catch ({ message, ...other}) {
logger.error(`=> skipping ~${path.relative(path.dirname(FIXTURES_ROOT), inputDirectory)}`)
logger.error(` ${message}${Object.keys(other).length > 0 ? ` (${JSON.stringify(other)})` : ''}\n`)
return undefined
}
})))
.then((trees) => trees.filter(Boolean) as ReadonlyArray<string>)
.then((trees) => {
const realTrees = trees.filter(Boolean)
const counts = {
invalid: trees.length - realTrees.length,
valid: realTrees.length,
total: trees.length,
unique: Object.keys(
realTrees.reduce((counts, tree) => {
counts[tree] = (counts[tree] || 0) + 1
return counts
}, {} as { [tree: string]: number })
).length
}

const { total, unique, valid, invalid } = counts
process.stdout.write(`
## Raw output
\`\`\`json
${JSON.stringify(counts, null, 2)}
\`\`\`
## Parsing statistics
This is the number of unique Abstract Syntax Trees after stripping commentary,
location data (whitespace) and other tokens.
| total | unique | valid | invalid |
|--------:|--------:|--------:|--------:|
| ${pad(total)} | ${pad(unique)} | ${pad(valid)} | ${pad(invalid)} |
`.trim())
})
8 changes: 8 additions & 0 deletions src/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"composite": true,
"extends": "../tsconfig.json",
"outDir": "./",
"include": [
"./"
]
}
3 changes: 0 additions & 3 deletions src/utils/all.ts

This file was deleted.

39 changes: 28 additions & 11 deletions src/utils/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,50 @@
import { Exercise } from '../exercise'
import { Solution } from '../solution'
import { ExecutionOptions } from './execution_options'
import { Logger, set as setGlobalLogger } from '../utils/logger'
import { ExecutionOptionsImpl } from './execution_options'
import { Logger, setProcessLogger as setGlobalLogger } from '../utils/logger'
import { DirectoryInput } from '../input/DirectoryInput';
import { GENERIC_FAILURE } from '../errors/codes';
import { ExerciseImpl } from '../ExerciseImpl';

export interface BootstrapResult {
exercise: Exercise
solution: Solution
input: DirectoryInput
options: ExecutionOptions
logger: Logger
}

/**
* The bootstrap call uses the arguments passed to the process to figure out
* which exercise to target, where the input lives (directory input) and what
* execution options to set.
*
* <entry> -dc two-fer ~/test/
*
* For example, if arguments are passed directly, the above will run the two-fer
* exercise analyzer with the ~/test/ input directory and turning on debug and
* console logging.
*/
export class Bootstrap {

/**
* Builds execution options, exercise and input based on the process arguments
*
*/
static call(): BootstrapResult {

process.on('uncaughtException', function(err) {
process.on('uncaughtException', function<T extends Error & { code?: number }>(err: T) {
console.error(err)
process.stderr.write(err.message)

process.exit(-1)
process.exit('code' in err ? err.code : GENERIC_FAILURE)
})

const options = ExecutionOptions.create()
const options = ExecutionOptionsImpl.create()
const logger = new Logger(options)
const exercise = new Exercise(options.exercise)
const solution = new Solution(options.inputDir, exercise)
const exercise = new ExerciseImpl(options.exercise)
const input = new DirectoryInput(options.inputDir, exercise.slug)

setGlobalLogger(logger)

return { exercise, solution, options, logger }
return { exercise, input, options, logger }
}
}

20 changes: 4 additions & 16 deletions src/utils/execution_options.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
import yargs from 'yargs'

interface ExecutionOptionsArgs {
debug: boolean
console: boolean
dry: boolean
output: string
inputDir: string
exercise: string
templates: boolean
}

export class ExecutionOptions {
export class ExecutionOptionsImpl implements ExecutionOptions {
public debug!: boolean
public console!: boolean
public output!: string
@@ -19,10 +9,8 @@ export class ExecutionOptions {
public dry!: boolean
public templates!: boolean

public constructor(options: ExecutionOptionsArgs) {
(Object.keys(options) as (keyof typeof options)[]).forEach((option) => {
this[option] = options[option]
})
public constructor(options: ExecutionOptions) {
Object.assign(this, options);
}

public static create() {
@@ -49,7 +37,7 @@ export class ExecutionOptions {
.argv

const { d, c, o, dry, templates, _ } = args
return new ExecutionOptions({
return new ExecutionOptionsImpl({
debug: d,
console: c,
output: o,
6 changes: 2 additions & 4 deletions src/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { ExecutionOptions } from './execution_options'

type StreamBuffer = string | Buffer | Uint8Array
type LoggerInput = StreamBuffer | (() => StreamBuffer)

@@ -38,14 +36,14 @@ const LIVE_BINDING: { current: Logger | null } = { current: null }
* @param logger
* @returns the global logger
*/
export function set(logger: Readonly<Logger>) {
export function setProcessLogger(logger: Readonly<Logger>) {
return LIVE_BINDING.current = logger
}

/**
* Get the 'global' logger
*/
export function get(): Logger {
export function getProcessLogger(): Logger {
return LIVE_BINDING.current!
}

37 changes: 37 additions & 0 deletions src/utils/runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { LogOutput } from '../output/processor/LogOutput'
import { FileOutput } from '../output/processor/FileOutput'
import { PassThroughOutput } from '../output/processor/PassThroughOutput'

/**
* Run a specific analyzer, given a set of execution options
*
* @param analyzer the analyzer to run
* @param input the input (source of the solution)
* @param options the options
*
* @returns the output
*
*/
export async function run(analyzer: Analyzer, input: Input, options: ExecutionOptions): Promise<Output> {
// This actually runs the analyzer and is the bases for any run. The options
// currently only affect the output.
const analysis = await analyzer.run(input)

// An output processor gets the Promise to the previous output processor and
// can add its own side-effects or transformation.
const processors: OutputProcessor[] = [

// Sends the output to the logger
LogOutput,

// Sends the output to a file
options.dry ? PassThroughOutput : FileOutput
]

return process(options, analysis, ...processors)
}

async function process(options: Readonly<ExecutionOptions>, analysis: Output, ...processors: OutputProcessor[]): Promise<Output> {
await processors.reduce((previous, processor) => processor(previous, options), analysis.toProcessable(options))
return analysis
}
16 changes: 8 additions & 8 deletions test/helpers/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ExecutionOptions } from "../../src/utils/execution_options";
import { Exercise } from "../../src/exercise";
import { set as setGlobalLogger, Logger } from "../../src/utils/logger";
import { BootstrapResult } from "../../dist/utils/bootstrap";
import { ExecutionOptionsImpl } from "../../src/utils/execution_options";
import { ExerciseImpl } from "../../src/ExerciseImpl";
import { BootstrapResult } from "../../src/utils/bootstrap";
import { setProcessLogger, Logger } from "../../src/utils/logger";

export function bootstrap({ exercise, ...overrides }: { exercise: string } & Partial<ExecutionOptions>): Omit<BootstrapResult, 'solution'> {
const options = new ExecutionOptions({
export function bootstrap({ exercise, ...overrides }: { exercise: string } & Partial<ExecutionOptions>): Omit<BootstrapResult, 'input'> {
const options = new ExecutionOptionsImpl({
debug: false,
console: false,
output: '__fake__',
@@ -15,11 +15,11 @@ export function bootstrap({ exercise, ...overrides }: { exercise: string } & Par
...overrides
})

const logger = setGlobalLogger(new Logger(options))
const logger = setProcessLogger(new Logger(options))

return {
options,
exercise: new Exercise(exercise),
exercise: new ExerciseImpl(exercise),
logger
}
}
29 changes: 0 additions & 29 deletions test/helpers/inline-solution.ts

This file was deleted.

23 changes: 23 additions & 0 deletions test/helpers/input/InlineInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

export class InlineInput implements Input {
/**
* Create a new solution reference
*
* @param rootDir the path to the root directory of the solution
* @param exercise the exercise this solution belongs to
*/
constructor(private readonly solutionFiles: string[]) { }

/**
* Read the solution file(s)
*
* @param n number of files to return
* @returns promise that resolves all the files at once
*/
public async read(n = 1): Promise<string[]> {
return Promise.all(
this.solutionFiles
.slice(0, n)
)
}
}
2 changes: 2 additions & 0 deletions test/ref.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// <reference path="../src/interface.d.ts" />
/// <reference path="../src/declarations.d.ts" />
33 changes: 17 additions & 16 deletions test/smoke.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Runner } from '../src/runner'
import { Analyzers } from '../src/analyzers'
import { TwoFerAnalyzer } from '../src/analyzers/two-fer'
import { InlineSolution } from './helpers/inline-solution'
import { run } from '../src/utils/runner'
import { find } from '../src/analyzers/Autoload'

import { bootstrap } from './helpers/bootstrap'
import { InlineInput } from './helpers/input/InlineInput'

const { options, exercise } = bootstrap({ exercise: 'two-fer' })

@@ -16,10 +17,10 @@ describe('When running analysis', () => {
};
`.trim()

const solution = new InlineSolution([solutionContent], exercise)
const analyzer = new TwoFerAnalyzer(solution)
const analyzer = new TwoFerAnalyzer()
const input = new InlineInput([solutionContent])
const output = await run(analyzer, input, options)

const output = await Runner.call(analyzer, options);
expect(output.status).toBe('approve_as_optimal');
expect(output.comments.length).toBe(0);
})
@@ -34,10 +35,10 @@ describe('When running analysis', () => {
export { twoFer }
`.trim()

const solution = new InlineSolution([solutionContent], exercise)
const analyzer = new TwoFerAnalyzer(solution)
const analyzer = new TwoFerAnalyzer()
const input = new InlineInput([solutionContent])
const output = await run(analyzer, input, options)

const output = await Runner.call(analyzer, options);
expect(output.status).toBe('approve_with_comment');
expect(output.comments.length).toBeGreaterThanOrEqual(1);
})
@@ -50,10 +51,10 @@ describe('When running analysis', () => {
};
`.trim()

const solution = new InlineSolution([solutionContent], exercise)
const analyzer = new TwoFerAnalyzer(solution)
const analyzer = new TwoFerAnalyzer()
const input = new InlineInput([solutionContent])
const output = await run(analyzer, input, options)

const output = await Runner.call(analyzer, options);
expect(output.status).toBe('disapprove_with_comment');
expect(output.comments.length).toBeGreaterThanOrEqual(1);
})
@@ -67,17 +68,17 @@ describe('When running analysis', () => {
};
`.trim()

const solution = new InlineSolution([solutionContent], exercise)
const analyzer = new TwoFerAnalyzer(solution)
const analyzer = new TwoFerAnalyzer()
const input = new InlineInput([solutionContent])
const output = await run(analyzer, input, options)

const output = await Runner.call(analyzer, options);
expect(output.status).toBe('refer_to_mentor');
})
})

describe('When autoloading analyzers', () => {
it('can find an analyzer based on an exercise', () => {
const ActualAnalyzer = Analyzers.find(exercise)
const ActualAnalyzer = find(exercise)
expect(ActualAnalyzer).toBe(TwoFerAnalyzer)
})
})
12 changes: 12 additions & 0 deletions test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"composite": true,
"extends": "../tsconfig.json",
"exclude": ["./fixtures"],
"compilerOptions": {
"noEmit": true,
},
"include": [
"./",
"../src"
]
}
1 change: 0 additions & 1 deletion test/types.d.ts

This file was deleted.

6 changes: 2 additions & 4 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"rootDirs": ["./src", "./"], /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
@@ -56,7 +56,5 @@
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
},
"include": ["src", "declarations.d.ts", "types.d.ts"],
"exclude": ["test/fixtures"]
}
}
104 changes: 68 additions & 36 deletions yarn.lock
Original file line number Diff line number Diff line change
@@ -860,26 +860,33 @@
dependencies:
"@types/jest-diff" "*"

"@types/node@^11.11.6":
version "11.11.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.6.tgz#df929d1bb2eee5afdda598a41930fe50b43eaa6a"
integrity sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==
"@types/node@^12.0.4":
version "12.0.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.4.tgz#46832183115c904410c275e34cf9403992999c32"
integrity sha512-j8YL2C0fXq7IONwl/Ud5Kt0PeXw22zGERt+HSSnwbKOJVsAGkEz3sFCYwaF9IOuoG1HOtE0vKCj6sXF7Q0+Vaw==

"@types/stack-utils@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==

"@types/yargs@^12.0.10":
version "12.0.10"
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.10.tgz#17a8ec65cd8e88f51b418ceb271af18d3137df67"
integrity sha512-WsVzTPshvCSbHThUduGGxbmnwcpkgSctHGHTqzWyFg4lYAuV5qXlyFPOsP3OWqCINfmg/8VXP+zJaa4OxEsBQQ==
"@types/yargs-parser@*":
version "13.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.0.0.tgz#453743c5bbf9f1bed61d959baab5b06be029b2d0"
integrity sha512-wBlsw+8n21e6eTd4yVv8YD/E3xq0O6nNnJIquutAsFGE7EyMKz7W6RNT6BRu1SmdgmlCZ9tb0X+j+D6HGr8pZw==

"@types/yargs@^12.0.2", "@types/yargs@^12.0.9":
version "12.0.12"
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916"
integrity sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw==

"@types/yargs@^13.0.0":
version "13.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.0.tgz#d2acb3bec0047d8f648ebacdab6b928a900c42c4"
integrity sha512-hY0o+kcz9M6kH32NUeb6VURghqMuCVkiUx+8Btsqhj4Hhov/hVGUx9DmBJeIkzlp1uAQK4wngQBCjqWdUUkFyA==
dependencies:
"@types/yargs-parser" "*"

"@typescript-eslint/experimental-utils@1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-1.9.0.tgz#a92777d0c92d7bc8627abd7cdb06cdbcaf2b39e8"
@@ -1296,6 +1303,15 @@ cliui@^4.0.0:
strip-ansi "^4.0.0"
wrap-ansi "^2.0.0"

cliui@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==
dependencies:
string-width "^3.1.0"
strip-ansi "^5.2.0"
wrap-ansi "^5.1.0"

co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -1620,10 +1636,10 @@ eslint-visitor-keys@^1.0.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==

eslint@^5.15.3:
version "5.15.3"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.15.3.tgz#c79c3909dc8a7fa3714fb340c11e30fd2526b8b5"
integrity sha512-vMGi0PjCHSokZxE0NLp2VneGw5sio7SSiDNgIUn2tC0XkWJRNOIoHIg3CliLVfXnJsiHxGAYrkw0PieAu8+KYQ==
eslint@^5.16.0:
version "5.16.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea"
integrity sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==
dependencies:
"@babel/code-frame" "^7.0.0"
ajv "^6.9.1"
@@ -1645,7 +1661,7 @@ eslint@^5.15.3:
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
inquirer "^6.2.2"
js-yaml "^3.12.0"
js-yaml "^3.13.0"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.3.0"
lodash "^4.17.11"
@@ -1662,10 +1678,10 @@ eslint@^5.15.3:
table "^5.2.3"
text-table "^0.2.0"

esm@^3.2.20:
version "3.2.20"
resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.20.tgz#44f125117863427cdece7223baa411fc739c1939"
integrity sha512-NA92qDA8C/qGX/xMinDGa3+cSPs4wQoFxskRrSnDo/9UloifhONFm4sl4G+JsyCqM007z2K+BfQlH5rMta4K1Q==
esm@^3.2.25:
version "3.2.25"
resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==

espree@^5.0.1:
version "5.0.1"
@@ -2797,10 +2813,10 @@ js-levenshtein@^1.1.3:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==

js-yaml@^3.12.0:
version "3.12.2"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.2.tgz#ef1d067c5a9d9cb65bd72f285b5d8105c77f14fc"
integrity sha512-QHn/Lh/7HhZ/Twc7vJYQTkjuCa0kaCcDcjK5Zlk2rvnUpy7DxMJ23+Jc2dcyvltwQVg1nygAVlB2oRDFHoRS5Q==
js-yaml@^3.13.0:
version "3.13.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"
@@ -4157,7 +4173,7 @@ string-width@^1.0.1:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"

string-width@^3.0.0:
string-width@^3.0.0, string-width@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
@@ -4194,6 +4210,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0:
dependencies:
ansi-regex "^4.1.0"

strip-ansi@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
dependencies:
ansi-regex "^4.1.0"

strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
@@ -4370,10 +4393,10 @@ type-check@~0.3.2:
dependencies:
prelude-ls "~1.1.2"

typescript@^3.4.5:
version "3.4.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.5.tgz#2d2618d10bb566572b8d7aad5180d84257d70a99"
integrity sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==
typescript@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.1.tgz#ba72a6a600b2158139c5dd8850f700e231464202"
integrity sha512-64HkdiRv1yYZsSe4xC1WVgamNigVYjlssIoaH2HcZF0+ijsk5YK2g0G34w9wJkze8+5ow4STd22AynfO6ZYYLw==

uglify-js@^3.1.4:
version "3.5.15"
@@ -4562,6 +4585,15 @@ wrap-ansi@^2.0.0:
string-width "^1.0.1"
strip-ansi "^3.0.1"

wrap-ansi@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
dependencies:
ansi-styles "^3.2.0"
string-width "^3.0.0"
strip-ansi "^5.0.0"

wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@@ -4613,10 +4645,10 @@ yargs-parser@^11.1.1:
camelcase "^5.0.0"
decamelize "^1.2.0"

yargs-parser@^13.0.0:
version "13.0.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.0.0.tgz#3fc44f3e76a8bdb1cc3602e860108602e5ccde8b"
integrity sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==
yargs-parser@^13.1.0:
version "13.1.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.0.tgz#7016b6dd03e28e1418a510e258be4bff5a31138f"
integrity sha512-Yq+32PrijHRri0vVKQEm+ys8mbqWjLiwQkMFNXEENutzLPP0bE4Lcd4iA3OQY5HF+GD3xXxf0MEHb8E4/SA3AA==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"
@@ -4639,12 +4671,12 @@ yargs@^12.0.2:
y18n "^3.2.1 || ^4.0.0"
yargs-parser "^11.1.1"

yargs@^13.2.2:
version "13.2.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.2.tgz#0c101f580ae95cea7f39d927e7770e3fdc97f993"
integrity sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==
yargs@^13.2.4:
version "13.2.4"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83"
integrity sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==
dependencies:
cliui "^4.0.0"
cliui "^5.0.0"
find-up "^3.0.0"
get-caller-file "^2.0.1"
os-locale "^3.1.0"
@@ -4654,4 +4686,4 @@ yargs@^13.2.2:
string-width "^3.0.0"
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^13.0.0"
yargs-parser "^13.1.0"