diff --git a/.babelrc b/.babelrc index 602445b2..9076ca75 100644 --- a/.babelrc +++ b/.babelrc @@ -8,13 +8,13 @@ "targets": { "node": "14.16" } - } ], "@babel/preset-typescript" ], "plugins": [ "@babel/plugin-proposal-optional-chaining", + "@babel/plugin-proposal-nullish-coalescing-operator", [ "module-resolver", { diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index c4911075..00000000 --- a/.eslintrc +++ /dev/null @@ -1,72 +0,0 @@ -{ - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./tsconfig.json", - "ecmaVersion": 2018, - "sourceType": "module", - "ecmaFeatures": { - "modules": true - } - }, - "env": { - "es6": true, - "node": true - }, - "plugins": ["import", "@typescript-eslint"], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "prettier", - "plugin:import/errors", - "plugin:import/warnings", - "plugin:import/typescript" - ], - "rules": { - "@typescript-eslint/explicit-function-return-type": [ - "warn", - { - "allowExpressions": false, - "allowTypedFunctionExpressions": true, - "allowHigherOrderFunctions": true - } - ], - "@typescript-eslint/explicit-member-accessibility": [ - "warn", - { - "accessibility": "no-public", - "overrides": { - "accessors": "explicit", - "constructors": "no-public", - "methods": "explicit", - "properties": "explicit", - "parameterProperties": "off" - } - } - ], - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-parameter-properties": [ - "warn", - { - "allows": [ - "private", - "protected", - "public", - "private readonly", - "protected readonly", - "public readonly" - ] - } - ], - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-use-before-define": [ - "error", - { - "functions": false, - "typedefs": false - } - ], - "import/no-unresolved": "off" - } -} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..4761bdfe --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + root: true, + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.eslint.json'], + }, + extends: '@exercism/eslint-config-tooling', + ignorePatterns: ['.eslintrc.js'], +} diff --git a/CHANGELOG.md b/CHANGELOG.md index d4bd5947..c2d70fed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.14.0 + +- Use `@exercism/eslint-config-tooling` for linting the files in this repository +- Use `@exercism/eslint-config-javascript` to lint (and report) on student code + ## 0.13.1 - Update dependencies diff --git a/README.md b/README.md index c385c656..99cb48f6 100644 --- a/README.md +++ b/README.md @@ -81,21 +81,6 @@ It will also format the output JSON with 2 space indentation, both in the output If you wish to _preview_ the actual messages, pass in `--noTemplates` to use the analyzer `Comment`Factories to generate actual messages. If the comment factories are kept in-sync with `website-copy`, it will be the exact same output as on the site. -### `batch` (.sh, .bat) - -```shell -./bin/batch.sh two-fer -cp -``` - -Runs all the fixtures in `~/test/fixtures/two-fer` through the analyzer, giving a summary at the end with all results. -This places an `analysis.json` in the source fixture folder. - -You'll most likely want `-cp` (`--console` and `--pretty`) during development, which enables console output (instead of `stdout`/`stderr`) and formats the -output JSON with 2 space indentation. - -If you wish to _preview_ the actual messages, pass in `--noTemplates` to use the analyzer `Comment`Factories to generate actual messages. -If the comment factories are kept in-sync with `website-copy`, it will be the exact same output as on the site. - ### `remote` (.sh, .bat) ```shell diff --git a/package.json b/package.json index c8495e40..8c533ca7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@exercism/javascript-analyzer", - "version": "0.13.2", + "version": "0.14.0", "description": "Exercism analyzer for javascript", "repository": "https://github.com/exercism/javascript-analyzer", "author": "Derk-Jan Karrenbeld ", @@ -38,27 +38,30 @@ "@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/preset-env": "^7.13.15", "@babel/preset-typescript": "^7.13.0", + "@exercism/eslint-config-tooling": "^0.1.0", "@tsconfig/recommended": "^1.0.1", + "@types/eslint": "^7.2.10", "@types/jest": "^26.0.22", - "@types/node": "^14.14.37", + "@types/node": "^14.14.41", "@types/yargs": "^16.0.1", - "@typescript-eslint/eslint-plugin": "^4.21.0", + "@typescript-eslint/eslint-plugin": "^4.22.0", "babel-jest": "^26.6.3", "babel-plugin-module-resolver": "^4.1.0", "core-js": "^3.10.1", - "eslint": "^7.24.0", - "eslint-config-prettier": "^8.1.0", + "eslint-config-prettier": "^8.2.0", "eslint-plugin-import": "^2.22.1", - "eslint-plugin-jest": "^24.3.4", + "eslint-plugin-jest": "^24.3.5", "jest": "^26.6.3", "rimraf": "^3.0.2", "shelljs": "^0.8.4" }, "dependencies": { - "@exercism/static-analysis": "^0.8.1", - "@typescript-eslint/parser": "^4.21.0", - "@typescript-eslint/typescript-estree": "^4.21.0", - "@typescript-eslint/visitor-keys": "^4.21.0", + "@exercism/eslint-config-javascript": "^0.3.1", + "@exercism/static-analysis": "^0.9.0", + "@typescript-eslint/parser": "^4.22.0", + "@typescript-eslint/typescript-estree": "^4.22.0", + "@typescript-eslint/visitor-keys": "^4.22.0", + "eslint": "^7.24.0", "typescript": "^4.2.4", "yargs": "^16.2.0" } diff --git a/src/analyze.ts b/src/analyze.ts index 8d82772f..7a419412 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -27,6 +27,7 @@ logger.log( // so it can be instantiated here. This allows us to add new analyzers without // needing to update a bookkeeping construct // +// eslint-disable-next-line @typescript-eslint/naming-convention const AnalyzerClass = find(exercise) const analyzer = new AnalyzerClass() @@ -39,4 +40,4 @@ const analyzer = new AnalyzerClass() // run(analyzer, input, options) .then(() => process.exit(0)) - .catch((err) => logger.fatal(err.toString())) + .catch((err: unknown) => logger.fatal(`${err}`)) diff --git a/src/analyzers/AnalyzerImpl.ts b/src/analyzers/AnalyzerImpl.ts index cd31076b..32317a4a 100644 --- a/src/analyzers/AnalyzerImpl.ts +++ b/src/analyzers/AnalyzerImpl.ts @@ -1,5 +1,5 @@ -import type { Input } from '@exercism/static-analysis' -import { getProcessLogger, Logger } from '@exercism/static-analysis' +import type { Input, Logger } from '@exercism/static-analysis' +import { getProcessLogger } from '@exercism/static-analysis' import type { Analyzer, Comment, Output } from '~src/interface' import { AnalyzerOutput } from '~src/output/AnalyzerOutput' @@ -47,7 +47,11 @@ export abstract class AnalyzerImpl implements Analyzer { await this.execute(input).catch((err): void | never => { if (err instanceof EarlyFinalization) { - this.logger.log(`=> early finialization (${this.output.status})`) + this.logger.log( + `=> early finalization (${ + this.output.summary ?? this.output.comments.length + })` + ) } else { throw err } diff --git a/src/analyzers/Autoload.ts b/src/analyzers/Autoload.ts index f7eb2fc3..aa5e9eca 100644 --- a/src/analyzers/Autoload.ts +++ b/src/analyzers/Autoload.ts @@ -13,14 +13,14 @@ type AnalyzerConstructor = new () => Analyzer export function find(exercise: Readonly): AnalyzerConstructor { const file = autoload(exercise) const key = Object.keys(file).find( - (key): boolean => file[key] instanceof Function + (fileKey): boolean => file[fileKey] instanceof Function ) if (key === undefined) { throw new Error(`No Analyzer found in './${exercise.slug}`) } - const analyzer = file[key] + const analyzer = file[key] as AnalyzerConstructor getProcessLogger().log(`=> analyzer: ${analyzer.name}`) return analyzer } @@ -35,22 +35,28 @@ class RequireError extends Error { } } -function autoload(exercise: Readonly): ReturnType { +function autoload(exercise: Readonly): Record { // explicit path (no extension) const modulePaths = [ path.join(__dirname, 'practice', exercise.slug, 'index'), path.join(__dirname, 'concept', exercise.slug, 'index'), + path.join(__dirname, 'generic', 'index'), ] const results = modulePaths.map((modulePath) => { try { - return require(modulePath) - } catch (err) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require(modulePath) as unknown + } catch (err: unknown) { return new RequireError(modulePath, err) } }) - if (results.every((result) => result instanceof RequireError)) { + if ( + results.every( + (result): result is RequireError => result instanceof RequireError + ) + ) { const slug = exercise.slug const logger = getProcessLogger() @@ -72,11 +78,14 @@ function autoload(exercise: Readonly): ReturnType { JSON.stringify( results.map((error) => ({ name: error.name, - cause: { - name: error.inner.name, - message: error.inner.message, - stack: error.inner.stack, - }, + cause: + error.inner instanceof Error + ? { + name: error.inner.name, + message: error.inner.message, + stack: error.inner.stack, + } + : error.inner, })), undefined, 2 @@ -85,5 +94,8 @@ function autoload(exercise: Readonly): ReturnType { ) } - return results.find((result) => !(result instanceof RequireError)) + return results.find((result) => !(result instanceof RequireError)) as Record< + string, + unknown + > } diff --git a/src/analyzers/IsolatedAnalyzerImpl.ts b/src/analyzers/IsolatedAnalyzerImpl.ts index aab9f114..5ba20c42 100644 --- a/src/analyzers/IsolatedAnalyzerImpl.ts +++ b/src/analyzers/IsolatedAnalyzerImpl.ts @@ -1,7 +1,6 @@ -import type { Input } from '@exercism/static-analysis' +import type { Input, Logger } from '@exercism/static-analysis' import { getProcessLogger, - Logger, NoSourceError, ParserError, } from '@exercism/static-analysis' @@ -61,7 +60,7 @@ export abstract class IsolatedAnalyzerImpl implements Analyzer { // The isolated analyzer output can use exceptions as control flow. // This block here explicitly accepts this. if (err instanceof EarlyFinalization) { - this.logger.log(`=> early finalization (${output.summary || '-'})`) + this.logger.log(`=> early finalization (${output.summary ?? '-'})`) } else { throw err } diff --git a/src/analyzers/SourceImpl.ts b/src/analyzers/SourceImpl.ts index 03a68ac5..c03b2f27 100644 --- a/src/analyzers/SourceImpl.ts +++ b/src/analyzers/SourceImpl.ts @@ -1,5 +1,6 @@ import { extractSource } from '@exercism/static-analysis' -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' +import type { TSESTree } from '@typescript-eslint/typescript-estree' +import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree' type NodeWithLocation = TSESTree.Node & { range?: TSESTree.Range @@ -7,7 +8,7 @@ type NodeWithLocation = TSESTree.Node & { } interface Source { - get(node: NodeWithLocation): string + get: (node: NodeWithLocation) => string } class SourceImpl implements Source { @@ -27,16 +28,10 @@ class SourceImpl implements Source { return this.get(node).replace(this.get(node.body), '...') } case AST_NODE_TYPES.FunctionDeclaration: { - return this.get(node).replace( - (node.body && this.get(node.body)) || '...', - '...' - ) + return this.get(node).replace(this.get(node.body) || '...', '...') } case AST_NODE_TYPES.FunctionExpression: { - return this.get(node).replace( - (node.body && this.get(node.body)) || '...', - '...' - ) + return this.get(node).replace(this.get(node.body) || '...', '...') } case AST_NODE_TYPES.VariableDeclaration: { const first = node.declarations[0].init diff --git a/src/analyzers/concept/__exemplar/ExemplarSolution.ts b/src/analyzers/concept/__exemplar/ExemplarSolution.ts index 0c14bac2..90f3451a 100644 --- a/src/analyzers/concept/__exemplar/ExemplarSolution.ts +++ b/src/analyzers/concept/__exemplar/ExemplarSolution.ts @@ -1,9 +1,10 @@ +import type { MetaConfiguration } from '@exercism/static-analysis' import { AstParser, extractExports, extractFunctions, } from '@exercism/static-analysis' -import { TSESTree } from '@typescript-eslint/typescript-estree' +import type { TSESTree } from '@typescript-eslint/typescript-estree' import { readFileSync } from 'fs' import path from 'path' import { Source } from '../../SourceImpl' @@ -22,9 +23,11 @@ export class ExemplarSolution { public readExemplar(directory: string): void { const configPath = path.join(directory, '.meta', 'config.json') - const config = JSON.parse(readFileSync(configPath).toString()) + const config = JSON.parse( + readFileSync(configPath).toString() + ) as MetaConfiguration - const exemplarPath = path.join(directory, config.files.exemplar[0]) + const exemplarPath = path.join(directory, (config.files.exemplar ?? [])[0]) this.exemplar = new Source(readFileSync(exemplarPath).toString()) } diff --git a/src/analyzers/concept/__exemplar/index.ts b/src/analyzers/concept/__exemplar/index.ts index 77e5add5..eb649afb 100644 --- a/src/analyzers/concept/__exemplar/index.ts +++ b/src/analyzers/concept/__exemplar/index.ts @@ -1,11 +1,11 @@ +import type { Input } from '@exercism/static-analysis' import { AstParser, - Input, NoExportError, NoMethodError, } from '@exercism/static-analysis' -import { TSESTree } from '@typescript-eslint/typescript-estree' -import { ExecutionOptions, WritableOutput } from '~src/interface' +import type { TSESTree } from '@typescript-eslint/typescript-estree' +import type { ExecutionOptions, WritableOutput } from '~src/interface' import { EXEMPLAR_SOLUTION, NO_METHOD, @@ -44,7 +44,7 @@ export class ExemplarAnalyzer extends IsolatedAnalyzerImpl { ): ExemplarSolution | never { try { return new ExemplarSolution(program, source) - } catch (error) { + } catch (error: unknown) { if (error instanceof NoMethodError) { output.add(NO_METHOD({ 'method.name': error.method })) output.finish() diff --git a/src/analyzers/concept/lasagna/LasagnaSolution.ts b/src/analyzers/concept/lasagna/LasagnaSolution.ts index 6170e447..9dfe3f8a 100644 --- a/src/analyzers/concept/lasagna/LasagnaSolution.ts +++ b/src/analyzers/concept/lasagna/LasagnaSolution.ts @@ -1,28 +1,25 @@ +import type { + ExtractedFunction, + IdentifierWithName, + MetaConfiguration, + ProgramConstant, + SpecificFunctionCall, +} from '@exercism/static-analysis' import { AstParser, - ExtractedExport, - ExtractedFunction, extractExports, extractFunctions, findFirst, - findLiteral, - findMemberCall, findRawLiteral, - findTopLevelConstants, guardCallExpression, guardIdentifier, guardLiteral, - IdentifierWithName, - ProgramConstant, - SpecificFunctionCall, - StructureError, traverse, } from '@exercism/static-analysis' -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' +import type { TSESTree } from '@typescript-eslint/typescript-estree' +import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree' import { readFileSync } from 'fs' import path from 'path' -import { assertNamedExport } from '~src/asserts/assert_named_export' -import { assertNamedFunction } from '~src/asserts/assert_named_function' import { assertPublicApi } from '../../../asserts/assert_public_api' import { assertPublicConstant } from '../../../asserts/assert_public_constant' import { Source } from '../../SourceImpl' @@ -154,12 +151,10 @@ class TotalTimeInMinutes { } public get hasCallToPreparationTime(): boolean { - return !!findFirst( - this.implementation.body, - ( - node - ): node is SpecificFunctionCall => - guardCallExpression(node, PREPARATION_TIME_IN_MINUTES) + return Boolean( + findFirst(this.implementation.body, (node): node is SpecificFunctionCall< + typeof PREPARATION_TIME_IN_MINUTES + > => guardCallExpression(node, PREPARATION_TIME_IN_MINUTES)) ) } } @@ -203,9 +198,11 @@ export class LasagnaSolution { public readExemplar(directory: string): void { const configPath = path.join(directory, '.meta', 'config.json') - const config = JSON.parse(readFileSync(configPath).toString()) + const config = JSON.parse( + readFileSync(configPath).toString() + ) as MetaConfiguration - const exemplarPath = path.join(directory, config.files.exemplar[0]) + const exemplarPath = path.join(directory, (config.files.exemplar ?? [])[0]) this.exemplar = new Source(readFileSync(exemplarPath).toString()) } diff --git a/src/analyzers/concept/lasagna/index.ts b/src/analyzers/concept/lasagna/index.ts index 30ff79b7..91ddc2a1 100644 --- a/src/analyzers/concept/lasagna/index.ts +++ b/src/analyzers/concept/lasagna/index.ts @@ -1,11 +1,11 @@ +import type { Input } from '@exercism/static-analysis' import { AstParser, - Input, NoExportError, NoMethodError, } from '@exercism/static-analysis' -import { TSESTree } from '@typescript-eslint/typescript-estree' -import { ExecutionOptions, WritableOutput } from '~src/interface' +import type { TSESTree } from '@typescript-eslint/typescript-estree' +import type { ExecutionOptions, WritableOutput } from '~src/interface' import { CommentType, factory } from '../../../comments/comment' import { EXEMPLAR_SOLUTION, @@ -120,7 +120,7 @@ export class LasagnaAnalyzer extends IsolatedAnalyzerImpl { ): LasagnaSolution | never { try { return new LasagnaSolution(program, source) - } catch (error) { + } catch (error: unknown) { if (error instanceof NoMethodError) { output.add(NO_METHOD({ 'method.name': error.method })) output.finish() diff --git a/src/analyzers/generic/index.ts b/src/analyzers/generic/index.ts new file mode 100644 index 00000000..c97d0017 --- /dev/null +++ b/src/analyzers/generic/index.ts @@ -0,0 +1,104 @@ +import type { FileInput, Input } from '@exercism/static-analysis' +import { + DirectoryInput, + DirectoryWithConfigInput, +} from '@exercism/static-analysis' +import type { TSESTree } from '@typescript-eslint/typescript-estree' +import type { Linter } from 'eslint' +import fs from 'fs' +import type { ExecutionOptions, WritableOutput } from '~src/interface' +import { IsolatedAnalyzerImpl } from '../IsolatedAnalyzerImpl' +import path from 'path' +import { spawnSync } from 'child_process' + +type Program = TSESTree.Program + +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +const SOLUTION_CONFIGURATION: Linter.Config = JSON.parse(` +{ + "root": true, + "extends": "@exercism/eslint-config-javascript" +} +`) + +const CONFIGURATION_OPTIONS = [ + '.eslintrc.js', + '.eslintrc.cjs', + '.eslintrc.yaml', + '.eslintrc.yml', + '.eslintrc.json', + '.eslintrc', +] + +export class GenericAnalyzer extends IsolatedAnalyzerImpl { + protected async execute( + input: Input, + output: WritableOutput, + options: ExecutionOptions + ): Promise { + // const linter = new Linter({ + // cwd: fs.existsSync(options.inputDir) ? options.inputDir : process.cwd(), + // }) + + if ( + !( + input instanceof DirectoryInput || + input instanceof DirectoryWithConfigInput + ) + ) { + this.logger.log('Can only run GenericAnalyzer of inputs with Directory') + output.finish() + } + + if (!fs.existsSync(options.outputDir)) { + this.logger.log( + `Can only run GenericAnalyzer if output directory ${options.outputDir} exists` + ) + output.finish() + } + + await new Promise((resolve) => { + fs.mkdir(path.join(options.outputDir, 'lint'), resolve) + }) + + // Read in the source files + const files: FileInput[] = await input.files() + + // Write the source files in a new directory + await Promise.all( + files.map(async (file) => { + const [data] = await file.read() + + return new Promise((resolve, reject) => { + fs.writeFile( + path.join(options.outputDir, 'lint', file.fileName), + data, + (err) => (err ? void reject(err) : void resolve(null)) + ) + }) + }) + ) + + // Write the new configuration + await new Promise((resolve, reject) => { + fs.writeFile( + path.join(options.outputDir, 'lint', '.eslintrc.js'), + JSON.stringify(SOLUTION_CONFIGURATION, undefined, 2), + (err) => (err ? void reject(err) : void resolve(null)) + ) + }) + + const root = path.resolve(__dirname, '..', '..', '..') + + // Run eslint + const result = spawnSync( + path.join(root, 'node_modules', '.bin', 'eslint'), + ['lint', '-c', path.join('.', 'lint', '.eslintrc')], + { cwd: options.outputDir, stdio: 'pipe' } + ) + + console.log(result.stdout, result.stderr, result.output) + + output.finish() + } +} diff --git a/src/analyzers/practice/gigasecond/GigasecondSolution.ts b/src/analyzers/practice/gigasecond/GigasecondSolution.ts index d91defb0..ad040042 100644 --- a/src/analyzers/practice/gigasecond/GigasecondSolution.ts +++ b/src/analyzers/practice/gigasecond/GigasecondSolution.ts @@ -1,6 +1,9 @@ -import { +import type { ExtractedExport, ExtractedFunction, + ProgramConstants, +} from '@exercism/static-analysis' +import { ExtractedVariable, extractExports, extractFunctions, @@ -13,9 +16,9 @@ import { guardIdentifier, guardLiteral, isNewExpression, - ProgramConstants, } from '@exercism/static-analysis' -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' +import type { TSESTree } from '@typescript-eslint/typescript-estree' +import { AST_NODE_TYPES } 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' @@ -45,7 +48,7 @@ class Constant { private _memoized: { [key: string]: string | number | boolean } constructor(private readonly constant: Readonly) { - this.name = constant.name || '' + this.name = constant.name ?? '' this._memoized = {} } @@ -65,7 +68,7 @@ class Constant { } if ('isOptimisedExpression' in this._memoized) { - return !!this._memoized['isOptimisedComprehension'] + return Boolean(this._memoized['isOptimisedComprehension']) } return (this._memoized[ @@ -81,7 +84,7 @@ class Constant { } if ('isLargeNumberLiteral' in this._memoized) { - return !!this._memoized['isLargeNumberLiteral'] + return Boolean(this._memoized['isLargeNumberLiteral']) } return (this._memoized['isLargeNumberLiteral'] = isLargeNumberLiteral(init)) @@ -89,7 +92,7 @@ class Constant { public isOptimal(): boolean { if ('isOptimal' in this._memoized) { - return !!this._memoized['isOptimal'] + return Boolean(this._memoized['isOptimal']) } const result = this.isOfKindConst && this.isOptimisedExpression @@ -106,7 +109,7 @@ class Entry { private readonly body: Node constructor(method: Readonly, source: Readonly) { - this.name = method.name || EXPECTED_METHOD + this.name = method.name ?? EXPECTED_METHOD this.params = method.params this.body = method.body @@ -126,19 +129,19 @@ class Entry { } public get hasDateParse(): boolean { - return !!findMemberCall(this.body, 'Date', 'parse') + return Boolean(findMemberCall(this.body, 'Date', 'parse')) } public get hasDateValueOnInput(): boolean { - return !!findMemberCall(this.body, this.parameterName, 'valueOf') + return Boolean(findMemberCall(this.body, this.parameterName, 'valueOf')) } public get hasGetSecondsOnInput(): boolean { - return !!findMemberCall(this.body, this.parameterName, 'getSeconds') + return Boolean(findMemberCall(this.body, this.parameterName, 'getSeconds')) } public get hasSetSecondsOnInput(): boolean { - return !!findMemberCall(this.body, this.parameterName, 'setSeconds') + return Boolean(findMemberCall(this.body, this.parameterName, 'setSeconds')) } public get parameterType(): Parameter['type'] { @@ -227,7 +230,7 @@ class Entry { if ( comprehension && - comprehension.type == AST_NODE_TYPES.VariableDeclarator + comprehension.type === AST_NODE_TYPES.VariableDeclarator ) { // Don't care about the kind because _there is no constant_. In that sense // it will not take into account the kind of the constant here. @@ -262,9 +265,11 @@ class Entry { : guardIdentifier(expression.right, constant.name) && expression.right logger.log( - `=> identifier: ${!!identifier}, expression: ${!!callExpression}` + `=> identifier: ${Boolean(identifier)}, expression: ${Boolean( + callExpression + )}` ) - return !!(callExpression && identifier) + return Boolean(callExpression && identifier) } // In this case the constant is just not extracted into the top-level, but @@ -272,19 +277,21 @@ class Entry { const comprehensionInExpression = expression.left === comprehension || expression.right === comprehension - return !!(callExpression && comprehensionInExpression) + return Boolean(callExpression && comprehensionInExpression) } } export class GigasecondSolution { public readonly source: Source - private mainMethod: Entry - private mainExport: ExtractedExport - private fileConstants: ProgramConstants - private mainConstant: Constant | undefined - private largeNumberComprehension: LargeNumberComprehension | undefined - private largeNumberLiteral: LargeNumberComprehension | undefined + private readonly mainMethod: Entry + private readonly mainExport: ExtractedExport + private readonly fileConstants: ProgramConstants + private readonly mainConstant: Constant | undefined + private readonly largeNumberComprehension: + | LargeNumberComprehension + | undefined + private readonly largeNumberLiteral: LargeNumberComprehension | undefined constructor(public readonly program: Program, source: string) { this.source = new Source(source) @@ -307,7 +314,6 @@ export class GigasecondSolution { // TODO upstream bug ] as unknown) as ['let']).filter( (declaration): boolean => - declaration && guardIdentifier(declaration.id) && declaration.id.name !== EXPECTED_METHOD ) @@ -318,19 +324,19 @@ export class GigasecondSolution { guardIdentifier(value.id) && (value.id.name.toUpperCase().includes('GIGASECOND') || value.id.name.toUpperCase().includes('MS')) - ) || this.fileConstants[0] + ) ?? this.fileConstants[0] - this.mainConstant = - (expectedConstant && - new Constant( + this.mainConstant = expectedConstant + ? new Constant( new ExtractedVariable( expectedConstant, expectedConstant.id, expectedConstant.kind, expectedConstant.init ) - )) || - undefined + ) + : undefined + this.largeNumberComprehension = findNumberComprehension(program) this.largeNumberLiteral = findNumberLiteral(program) } @@ -403,7 +409,7 @@ function findNumberComprehension( case AST_NODE_TYPES.AssignmentExpression: return isOptimisedComprehension(node.right) case AST_NODE_TYPES.AssignmentPattern: - return node.right !== undefined && isOptimisedComprehension(node.right) + return isOptimisedComprehension(node.right) case AST_NODE_TYPES.BinaryExpression: return isOptimisedComprehension(node) case AST_NODE_TYPES.CallExpression: @@ -417,7 +423,7 @@ function findNumberComprehension( default: return false } - }) as LargeNumberComprehension | undefined + }) } function isOptimisedComprehension(expression: Expression): boolean { @@ -529,7 +535,7 @@ function findNumberLiteral( case AST_NODE_TYPES.AssignmentExpression: return isLargeNumberLiteral(node.right) case AST_NODE_TYPES.AssignmentPattern: - return node.right !== undefined && isLargeNumberLiteral(node.right) + return isLargeNumberLiteral(node.right) case AST_NODE_TYPES.BinaryExpression: return isLargeNumberLiteral(node) case AST_NODE_TYPES.ExpressionStatement: @@ -541,7 +547,7 @@ function findNumberLiteral( default: return false } - }) as LargeNumberComprehension | undefined + }) } function isLargeNumberLiteral(node: TSESTree.Node): boolean { diff --git a/src/analyzers/practice/gigasecond/index.ts b/src/analyzers/practice/gigasecond/index.ts index 57f8dbdd..cd903893 100644 --- a/src/analyzers/practice/gigasecond/index.ts +++ b/src/analyzers/practice/gigasecond/index.ts @@ -1,11 +1,12 @@ +import type { Input } from '@exercism/static-analysis' import { AstParser, guardIdentifier, - Input, NoExportError, NoMethodError, } from '@exercism/static-analysis' -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' +import type { TSESTree } from '@typescript-eslint/typescript-estree' +import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree' import { IsolatedAnalyzerImpl } from '~src/analyzers/IsolatedAnalyzerImpl' import { CommentType, factory } from '~src/comments/comment' import { @@ -136,7 +137,7 @@ export class GigasecondAnalyzer extends IsolatedAnalyzerImpl { ): GigasecondSolution | never { try { return new GigasecondSolution(program, source) - } catch (error) { + } catch (error: unknown) { if (error instanceof NoMethodError) { output.disapprove(NO_METHOD({ 'method.name': error.method })) } diff --git a/src/analyzers/practice/resistor-color-duo/ResistorColorDuoSolution.ts b/src/analyzers/practice/resistor-color-duo/ResistorColorDuoSolution.ts index e455c784..7df10c32 100644 --- a/src/analyzers/practice/resistor-color-duo/ResistorColorDuoSolution.ts +++ b/src/analyzers/practice/resistor-color-duo/ResistorColorDuoSolution.ts @@ -1,6 +1,13 @@ -import { +import type { ExtractedExport, ExtractedFunction, + Logger, + ProgramConstants, + SpecificFunctionCall, + SpecificObjectPropertyCall, + SpecificPropertyCall, +} from '@exercism/static-analysis' +import { ExtractedVariable, extractExports, extractFunctions, @@ -14,13 +21,9 @@ import { guardLiteral, guardMemberExpression, guardTemplateLiteral, - Logger, - ProgramConstants, - SpecificFunctionCall, - SpecificObjectPropertyCall, - SpecificPropertyCall, } from '@exercism/static-analysis' -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' +import type { TSESTree } from '@typescript-eslint/typescript-estree' +import { AST_NODE_TYPES } 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' @@ -87,7 +90,7 @@ class Constant { private readonly constant: Readonly, source: Source ) { - this.name = constant.name || '' + this.name = constant.name ?? '' this.signature = source.getOuter(constant.node) } @@ -149,7 +152,7 @@ class Constant { public isOptimalObject( node: ExtractedVariable | undefined = this.constant ): boolean { - if (!node || !node.init) { + if (!node.init) { return false } @@ -229,7 +232,7 @@ class Entry { private lastIssue_: Issue constructor(method: Readonly, source: Readonly) { - this.name = method.name || EXPECTED_METHOD + this.name = method.name ?? EXPECTED_METHOD this.params = method.params this.body = method.body @@ -255,12 +258,12 @@ class Entry { public get hasOptimalParameter(): boolean { const [param] = this.params return ( - !!param && + Boolean(param) && param.type === AST_NODE_TYPES.ArrayPattern && param.elements.length === 2 && - !!param.elements[0] && + Boolean(param.elements[0]) && guardIdentifier(param.elements[0]) && - !!param.elements[1] && + Boolean(param.elements[1]) && guardIdentifier(param.elements[1]) ) } @@ -524,7 +527,7 @@ class Entry { // if (helperMethodName) { const helperDeclaration = - extractNamedFunction(helperMethodName, program) || + extractNamedFunction(helperMethodName, program) ?? extractNamedFunction(helperMethodName, body) if (!helperDeclaration) { logger.log(`~> could not find helper ${helperMethodName}`) @@ -605,7 +608,7 @@ class Entry { } const helperDeclaration = - extractNamedFunction(helperMethodName, program) || + extractNamedFunction(helperMethodName, program) ?? extractNamedFunction(helperMethodName, body) if (!helperDeclaration) { logger.log(`~> could not find helper ${helperMethodName}`) @@ -690,9 +693,7 @@ class Entry { constant?: Readonly ): boolean { logger.log( - `~> reduce optimal check is not implemented: ${body.type} (${ - constant && constant.name - })` + `~> reduce optimal check is not implemented: ${body.type} (${constant?.name})` ) // colors.slice(0, 2).reduce((acc, color) => acc * 10 + colorCode(code), 0) // colors.slice(0, 2).reverse().reduce((acc, color, index) => acc + colorCode(code) * (10 ** index), 0) @@ -796,7 +797,7 @@ class Entry { } const helperDeclaration = - extractNamedFunction(helperMethodName, program) || + extractNamedFunction(helperMethodName, program) ?? extractNamedFunction(helperMethodName, body) if (!helperDeclaration) { logger.log(`~> could not find helper ${helperMethodName}`) @@ -1005,10 +1006,10 @@ class Entry { export class ResistorColorDuoSolution { public readonly source: Source - private mainMethod: Entry - private mainExport: ExtractedExport - private fileConstants: ProgramConstants - private mainConstant: Constant | undefined + private readonly mainMethod: Entry + private readonly mainExport: ExtractedExport + private readonly fileConstants: ProgramConstants + private readonly mainConstant: Constant | undefined constructor(public readonly program: Program, source: string) { this.source = new Source(source) @@ -1039,7 +1040,7 @@ export class ResistorColorDuoSolution { const expectedConstant = this.fileConstants.find((constant) => guardIdentifier(constant.id, PROBABLE_CONSTANT) - ) || + ) ?? // Or find the first array or object assignment this.fileConstants.find( (constant) => @@ -1047,7 +1048,7 @@ export class ResistorColorDuoSolution { [ AST_NODE_TYPES.ArrayExpression, AST_NODE_TYPES.ObjectExpression, - ].indexOf(constant.init.type) !== -1 + ].includes(constant.init.type) ) this.mainConstant = diff --git a/src/analyzers/practice/resistor-color-duo/index.ts b/src/analyzers/practice/resistor-color-duo/index.ts index 0b06f521..4342b43a 100644 --- a/src/analyzers/practice/resistor-color-duo/index.ts +++ b/src/analyzers/practice/resistor-color-duo/index.ts @@ -1,10 +1,10 @@ +import type { Input } from '@exercism/static-analysis' import { AstParser, - Input, NoExportError, NoMethodError, } from '@exercism/static-analysis' -import { TSESTree } from '@typescript-eslint/typescript-estree' +import type { TSESTree } from '@typescript-eslint/typescript-estree' import { IsolatedAnalyzerImpl } from '~src/analyzers/IsolatedAnalyzerImpl' import { CommentType, factory } from '~src/comments/comment' import { @@ -13,7 +13,7 @@ import { NO_PARAMETER, UNEXPECTED_PARAMETER, } from '~src/comments/shared' -import { WritableOutput } from '~src/interface' +import type { WritableOutput } from '~src/interface' import { HelperCallNotFound, HelperNotOptimal, @@ -155,7 +155,7 @@ export class ResistorColorDuoAnalyzer extends IsolatedAnalyzerImpl { ): ResistorColorDuoSolution | never { try { return new ResistorColorDuoSolution(program, source) - } catch (error) { + } catch (error: unknown) { if (error instanceof NoMethodError) { output.disapprove(NO_METHOD({ 'method.name': error.method })) } @@ -286,9 +286,7 @@ export class ResistorColorDuoAnalyzer extends IsolatedAnalyzerImpl { solution: ResistorColorDuoSolution, output: WritableOutput ): void | never { - if (solution || output) { - return - } + // } private checkForDisapprovables( @@ -330,9 +328,7 @@ export class ResistorColorDuoAnalyzer extends IsolatedAnalyzerImpl { output.disapprove() } - if (solution || output) { - return - } + // } private checkForTips( diff --git a/src/analyzers/practice/resistor-color/ResistorColorSolution.ts b/src/analyzers/practice/resistor-color/ResistorColorSolution.ts index 20b09852..464021d5 100644 --- a/src/analyzers/practice/resistor-color/ResistorColorSolution.ts +++ b/src/analyzers/practice/resistor-color/ResistorColorSolution.ts @@ -1,6 +1,11 @@ -import { +import type { ExtractedExport, ExtractedFunction, + ProgramConstant, + ProgramConstants, + SpecificPropertyCall, +} from '@exercism/static-analysis' +import { extractExports, extractFunctions, findFirst, @@ -10,11 +15,9 @@ import { guardIdentifier, guardLiteral, guardMemberExpression, - ProgramConstant, - ProgramConstants, - SpecificPropertyCall, } from '@exercism/static-analysis' -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' +import type { TSESTree } from '@typescript-eslint/typescript-estree' +import { AST_NODE_TYPES } 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' @@ -42,9 +45,8 @@ class Constant { source: Source ) { this.name = - (constant && guardIdentifier(constant.id) && constant.id.name) || - '' - this.signature = source.getOuter(constant.parent || constant) + (guardIdentifier(constant.id) && constant.id.name) || '' + this.signature = source.getOuter(constant.parent ?? constant) } public get kind(): ProgramConstant['kind'] { @@ -103,7 +105,7 @@ class Constant { } public isOptimalObject(node = this.constant): boolean { - if (!node || !node.init) { + if (!node.init) { return false } @@ -172,7 +174,7 @@ class Constant { const name = this.referencedSourceObjectName return constants.find( (constant): boolean => - constant && guardIdentifier(constant.id) && constant.id.name === name + guardIdentifier(constant.id) && constant.id.name === name ) } } @@ -185,7 +187,7 @@ class Entry { private readonly body: Node constructor(method: Readonly, source: Readonly) { - this.name = method.name || EXPECTED_METHOD + this.name = method.name ?? EXPECTED_METHOD this.params = method.params this.body = method.body @@ -376,13 +378,13 @@ class Entry { export class ResistorColorSolution { public readonly source: Source - private mainMethod: Entry - private mainExports: { + private readonly mainMethod: Entry + private readonly mainExports: { function: ExtractedExport constant: ExtractedExport } - private fileConstants: ProgramConstants - private mainConstant: Constant | undefined + private readonly fileConstants: ProgramConstants + private readonly mainConstant: Constant | undefined constructor(public readonly program: Program, source: string) { this.source = new Source(source) @@ -408,7 +410,6 @@ export class ResistorColorSolution { // TODO: bug in upstream ] as unknown) as ['let']).filter( (declaration): boolean => - declaration && guardIdentifier(declaration.id) && declaration.id.name !== EXPECTED_METHOD ) @@ -417,15 +418,15 @@ export class ResistorColorSolution { const expectedConstant = this.fileConstants.find((constant) => guardIdentifier(constant.id, EXPECTED_CONSTANT) - ) || + ) ?? // Or find the first array or object assignment this.fileConstants.find( (constant) => constant.init && - [ + ![ AST_NODE_TYPES.ArrayExpression, AST_NODE_TYPES.ObjectExpression, - ].indexOf(constant.init.type) === -1 + ].includes(constant.init.type) ) this.mainConstant = diff --git a/src/analyzers/practice/resistor-color/index.ts b/src/analyzers/practice/resistor-color/index.ts index 0cd06305..6ae2fb09 100644 --- a/src/analyzers/practice/resistor-color/index.ts +++ b/src/analyzers/practice/resistor-color/index.ts @@ -1,10 +1,10 @@ +import type { Input } from '@exercism/static-analysis' import { AstParser, - Input, NoExportError, NoMethodError, } from '@exercism/static-analysis' -import { TSESTree } from '@typescript-eslint/typescript-estree' +import type { TSESTree } from '@typescript-eslint/typescript-estree' import { IsolatedAnalyzerImpl } from '~src/analyzers/IsolatedAnalyzerImpl' import { CommentType, factory } from '~src/comments/comment' import { @@ -13,7 +13,7 @@ import { NO_PARAMETER, UNEXPECTED_PARAMETER, } from '~src/comments/shared' -import { WritableOutput } from '~src/interface' +import type { WritableOutput } from '~src/interface' import { ResistorColorSolution } from './ResistorColorSolution' const TIP_EXPORT_INLINE = factory<'method.signature' | 'constant.signature'>` @@ -108,7 +108,7 @@ export class ResistorColorAnalyzer extends IsolatedAnalyzerImpl { ): ResistorColorSolution | never { try { return new ResistorColorSolution(program, source) - } catch (error) { + } catch (error: unknown) { if (error instanceof NoMethodError) { output.disapprove(NO_METHOD({ 'method.name': error.method })) } @@ -183,9 +183,7 @@ export class ResistorColorAnalyzer extends IsolatedAnalyzerImpl { solution: ResistorColorSolution, output: WritableOutput ): void | never { - if (solution || output) { - return - } + // } private checkForDisapprovables( @@ -215,9 +213,7 @@ export class ResistorColorAnalyzer extends IsolatedAnalyzerImpl { output.disapprove() } - if (solution || output) { - return - } + // } private checkForTips( diff --git a/src/analyzers/practice/two-fer/index.ts b/src/analyzers/practice/two-fer/index.ts index 9a32859c..79b363b6 100644 --- a/src/analyzers/practice/two-fer/index.ts +++ b/src/analyzers/practice/two-fer/index.ts @@ -1,6 +1,10 @@ +import type { + ExtractedFunction, + Input, + ParsedSource, +} from '@exercism/static-analysis' import { AstParser, - ExtractedFunction, extractExports, findAll, findFirstOfType, @@ -11,12 +15,11 @@ import { guardLogicalExpression, guardTemplateLiteral, guardUnaryExpression, - Input, NoSourceError, - ParsedSource, ParserError, } from '@exercism/static-analysis' -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' +import type { TSESTree } from '@typescript-eslint/typescript-estree' +import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree' import { AnalyzerImpl } from '~src/analyzers/AnalyzerImpl' import { parameterName } from '~src/analyzers/utils/extract_parameter' import { annotateType } from '~src/analyzers/utils/type_annotations' @@ -124,16 +127,20 @@ export class TwoFerAnalyzer extends AnalyzerImpl { private async parse(input: Input): never | Promise { try { return await AstParser.ANALYZER.parse(input) - } catch (err) { + } catch (err: unknown) { // Here we handle errors that blew up the analyzer but we don't want to // report as blown up. This converts these errors to the commentary. if (err instanceof NoSourceError) { const output = makeNoSourceOutput(err) - output.comments.forEach((comment) => this.comment(comment)) + output.comments.forEach((comment) => { + this.comment(comment) + }) this.redirect() } else if (err instanceof ParserError) { const output = makeParseErrorOutput(err) - output.comments.forEach((comment) => this.comment(comment)) + output.comments.forEach((comment) => { + this.comment(comment) + }) this.redirect() } @@ -291,6 +298,7 @@ export class TwoFerAnalyzer extends AnalyzerImpl { this.comment( OPTIMISE_EXPLICIT_DEFAULT_VALUE({ parameter, + // eslint-disable-next-line @typescript-eslint/naming-convention maybe_undefined_expression: expression.left.name, }) ) @@ -321,6 +329,7 @@ export class TwoFerAnalyzer extends AnalyzerImpl { this.comment( OPTIMISE_EXPLICIT_DEFAULT_VALUE({ parameter, + // eslint-disable-next-line @typescript-eslint/naming-convention maybe_undefined_expression: conditionalExpression.consequent.name, }) ) @@ -519,9 +528,11 @@ export class TwoFerAnalyzer extends AnalyzerImpl { // export { gigasecond } // => yes specififers // - return !!extractExports(this.program).find( - (extracted) => - extracted.exported === 'twoFer' && extracted.exportKind === 'value' + return Boolean( + extractExports(this.program).find( + (extracted) => + extracted.exported === 'twoFer' && extracted.exportKind === 'value' + ) ) } @@ -537,7 +548,7 @@ export class TwoFerAnalyzer extends AnalyzerImpl { this.mainMethod.node, AST_NODE_TYPES.TemplateLiteral ) - return !!( + return Boolean( template && template.quasis.length + template.expressions.length === 3 ) } diff --git a/src/analyzers/utils/extract_main_method.ts b/src/analyzers/utils/extract_main_method.ts deleted file mode 100644 index 6232d55b..00000000 --- a/src/analyzers/utils/extract_main_method.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { guardIdentifier } from '@exercism/static-analysis' -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' -import { extractNamedFunction } from '~src/extracts/extract_named_function' - -type Program = TSESTree.Program -type Node = TSESTree.Node - -type ArrowFunctionExpression = TSESTree.ArrowFunctionExpression -type FunctionDeclaration = TSESTree.FunctionDeclaration -type FunctionExpression = TSESTree.FunctionExpression -type Identifier = TSESTree.Identifier - -type AnyMainMethodNode = - | FunctionDeclaration - | ArrowFunctionExpression - | FunctionExpression - -/** - * @deprecated use extractNamedFunction instead - */ -export type MainMethod< - T extends string = string, - TNode extends AnyMainMethodNode = AnyMainMethodNode -> = { - id: Identifier & { name: T } - parent: undefined | Node -} & TNode - -export function extractMainMethod( - program: Program, - name: T -): MainMethod | undefined { - const fn = extractNamedFunction(name, program) - if (!fn) { - return undefined - } - - const { node } = fn - - switch (node.type) { - case AST_NODE_TYPES.FunctionDeclaration: { - if (!guardIdentifier(node.id)) { - return undefined - } - - return { - ...node, - parent: undefined, - id: node.id as Identifier & { name: T }, - } - } - case AST_NODE_TYPES.ArrowFunctionExpression: { - const { id, ...rest } = node - - return { - ...rest, - id: { - type: AST_NODE_TYPES.Identifier, - name, - loc: node.loc, - range: node.range, - }, - } as MainMethod - } - case AST_NODE_TYPES.FunctionExpression: { - const { id, ...rest } = node - - return { - ...rest, - id: { - type: AST_NODE_TYPES.Identifier, - name, - loc: node.loc, - range: node.range, - }, - } as MainMethod - } - } - - return undefined -} diff --git a/src/analyzers/utils/extract_parameter.ts b/src/analyzers/utils/extract_parameter.ts index 36f94134..abd55f10 100644 --- a/src/analyzers/utils/extract_parameter.ts +++ b/src/analyzers/utils/extract_parameter.ts @@ -1,4 +1,5 @@ -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' +import type { TSESTree } from '@typescript-eslint/typescript-estree' +import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree' type Parameter = TSESTree.Parameter type ObjectLiteralElementLike = TSESTree.ObjectLiteralElementLike @@ -128,10 +129,7 @@ function expressionName( case AST_NODE_TYPES.ArrayPattern: case AST_NODE_TYPES.ObjectPattern: case AST_NODE_TYPES.Identifier: - // Disabled this rule here because we _want_ the fall-through - // eslint-disable-next-line no-case-declarations - const result = parameterName(element, fallback) - return Array.isArray(result) ? result[0] : result + return parameterName(element, fallback) // Don't know how to get the name default: diff --git a/src/analyzers/utils/test_parameter.ts b/src/analyzers/utils/test_parameter.ts index d4900811..f11574c1 100644 --- a/src/analyzers/utils/test_parameter.ts +++ b/src/analyzers/utils/test_parameter.ts @@ -1,4 +1,5 @@ -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' +import type { TSESTree } from '@typescript-eslint/typescript-estree' +import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree' type Parameter = TSESTree.Parameter @@ -8,7 +9,7 @@ export function isOptional(parameter: Parameter): boolean { case AST_NODE_TYPES.Identifier: // arg?: type case AST_NODE_TYPES.ObjectPattern: // { arg }?: type case AST_NODE_TYPES.RestElement: // ...arg?: type - return parameter.optional || false + return parameter.optional ?? false // (...)?: type = expression case AST_NODE_TYPES.AssignmentPattern: { diff --git a/src/analyzers/utils/type_annotations.ts b/src/analyzers/utils/type_annotations.ts index 78eff558..18853e68 100644 --- a/src/analyzers/utils/type_annotations.ts +++ b/src/analyzers/utils/type_annotations.ts @@ -1,4 +1,5 @@ -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree' +import type { TSESTree } from '@typescript-eslint/typescript-estree' +import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree' import { parameterName } from './extract_parameter' type TSTypeAnnotation = TSESTree.TSTypeAnnotation @@ -80,7 +81,7 @@ function annotate(typeNode?: TypeNode, fallback = 'any'): string { return 'any' } case AST_NODE_TYPES.TSArrayType: { - return `Array<${typeNode.elementType}>` + return `Array<${typeNode.elementType.type}>` } case AST_NODE_TYPES.TSBigIntKeyword: { return 'bigint' @@ -125,7 +126,7 @@ function annotate(typeNode?: TypeNode, fallback = 'any'): string { } case AST_NODE_TYPES.TSImportType: { return `${typeNode.isTypeOf ? 'typeof ' : ''} import(${ - typeNode.parameter + typeNode.parameter.type })${ typeNode.qualifier ? `.${annotateEntityName(typeNode.qualifier)}` : '' } <...>` // todo type parameters @@ -209,7 +210,7 @@ function annotate(typeNode?: TypeNode, fallback = 'any'): string { )}` } case AST_NODE_TYPES.TSTypePredicate: { - return `${typeNode.parameterName} is ${annotateType( + return `${typeNode.parameterName.type} is ${annotateType( typeNode.typeAnnotation, fallback )}` @@ -218,7 +219,7 @@ function annotate(typeNode?: TypeNode, fallback = 'any'): string { return `typeof ${annotateEntityName(typeNode.exprName)}` } case AST_NODE_TYPES.TSTypeReference: { - return `${typeNode.typeName}<...>` // TODO type parameters + return `${typeNode.typeName.type}<...>` // TODO type parameters } case AST_NODE_TYPES.TSUndefinedKeyword: { return 'undefined' diff --git a/src/asserts/assert_named_export.ts b/src/asserts/assert_named_export.ts index 6a1ea970..71f6be80 100644 --- a/src/asserts/assert_named_export.ts +++ b/src/asserts/assert_named_export.ts @@ -1,4 +1,5 @@ -import { ExtractedExport, NoExportError } from '@exercism/static-analysis' +import type { ExtractedExport } from '@exercism/static-analysis' +import { NoExportError } from '@exercism/static-analysis' import type { TSESTree } from '@typescript-eslint/typescript-estree' import { extractNamedExport } from '~src/extracts/extract_named_export' @@ -19,7 +20,7 @@ export function assertNamedExport( ): ExtractedExport | never { // Find the export const fn = Array.isArray(root) - ? root.find((fn) => fn.exported === exported) + ? (root as readonly ExtractedExport[]).find((e) => e.exported === exported) : extractNamedExport(exported, root as Node) // Does it exist? diff --git a/src/asserts/assert_named_function.ts b/src/asserts/assert_named_function.ts index 48bcb3fd..24f6dc68 100644 --- a/src/asserts/assert_named_function.ts +++ b/src/asserts/assert_named_function.ts @@ -1,4 +1,5 @@ -import { ExtractedFunction, NoMethodError } from '@exercism/static-analysis' +import type { ExtractedFunction } from '@exercism/static-analysis' +import { NoMethodError } from '@exercism/static-analysis' import type { TSESTree } from '@typescript-eslint/typescript-estree' import { extractNamedFunction } from '~src/extracts/extract_named_function' @@ -19,7 +20,7 @@ export function assertNamedFunction( ): ExtractedFunction | never { // Find the function const fn = Array.isArray(root) - ? root.find((fn) => fn.name === name) + ? (root as ExtractedFunction[]).find((f) => f.name === name) : extractNamedFunction(name, root as Node) // Does it exist? diff --git a/src/asserts/assert_public_api.ts b/src/asserts/assert_public_api.ts index 60266e74..41fcb69c 100644 --- a/src/asserts/assert_public_api.ts +++ b/src/asserts/assert_public_api.ts @@ -1,4 +1,7 @@ -import { ExtractedExport, ExtractedFunction } from '@exercism/static-analysis' +import type { + ExtractedExport, + ExtractedFunction, +} from '@exercism/static-analysis' import { assertNamedExport } from './assert_named_export' import { assertNamedFunction } from './assert_named_function' diff --git a/src/asserts/assert_public_constant.ts b/src/asserts/assert_public_constant.ts index 019bc183..f8c14585 100644 --- a/src/asserts/assert_public_constant.ts +++ b/src/asserts/assert_public_constant.ts @@ -1,11 +1,13 @@ -import { +import type { ExtractedExport, + ProgramConstant, +} from '@exercism/static-analysis' +import { findTopLevelConstants, guardIdentifier, - ProgramConstant, StructureError, } from '@exercism/static-analysis' -import { TSESTree } from '@typescript-eslint/typescript-estree' +import type { TSESTree } from '@typescript-eslint/typescript-estree' import { assertNamedExport } from '../asserts/assert_named_export' export class NoPublicConstantError extends StructureError { diff --git a/src/batch.ts b/src/batch.ts deleted file mode 100644 index 165c9528..00000000 --- a/src/batch.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { DirectoryInput } from '@exercism/static-analysis/dist/input/DirectoryInput' -import { readDir } from '@exercism/static-analysis/dist/utils/fs' -import path from 'path' -import { find } from './analyzers/Autoload' -import type { Comment, Output } from './interface' -import { FileOutput } from './output/processor/FileOutput' -import { Bootstrap } from './utils/bootstrap' - -// 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 = 20, padc = ' '): string { - const pad = Array(length).fill(padc).join('') - return (pad + value).slice(-length) -} - -const DEFAULT_LINE_DATA = { - count: 0, - comments: { - unique: [] as string[], - uniqueTemplates: [] 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: typeof DEFAULT_LINE_DATA = DEFAULT_LINE_DATA -): string { - const { - count, - comments: { unique, uniqueTemplates }, - runtimes: { total, average, median }, - } = data - return `| ${[ - pad(humanStatus, 20), - pad(count, 5), - pad(unique.length, 8), - pad(uniqueTemplates.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 - - const fixture = fixtureDir - - const processable = analysis.toProcessable(options) - - if (options.dry) { - await processable - } else { - await FileOutput(processable, { - ...options, - inputDir, - output: './analysis.json', - }) - } - - return { result: analysis, runtime, fixture } - } catch (_ignore) { - return undefined - } - }) - ) - ) - .then( - (results) => - results.filter(Boolean) as readonly { - result: Output - runtime: bigint - fixture: string - }[] - ) - .then((results) => { - return results.reduce( - (groups, { result: { status, comments }, runtime, fixture }) => { - groups[status] = groups[status] || { - runtimes: [], - comments: [], - count: 0, - fixtures: [], - } - - groups[status].runtimes.push(runtime) - groups[status].comments.push(...comments) - groups[status].count += 1 - groups[status].fixtures.push(fixture) - - return groups - }, - {} as { - [K in Output['status']]: { - runtimes: bigint[] - count: number - comments: Comment[] - fixtures: string[] - } - } - ) - }) - .then((grouped) => { - const aggregatedGroups = (Object.keys( - grouped - ) as Output['status'][]).reduce((aggregated, status) => { - const { count, comments, runtimes, fixtures } = grouped[status] - - const sortedRuntimes = runtimes.sort() - - const totalRuntime = runtimes.reduce( - (result, time): bigint => 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): string => comment.message) - ), - ] - const uniqueTemplates = [ - ...new Set( - comments.filter(Boolean).map((comment): string => comment.template) - ), - ] - - return { - ...aggregated, - [status]: { - count, - comments: { - unique: uniqueComments, - uniqueTemplates: uniqueTemplates, - }, - runtimes: { - total: totalRuntime, - average: averageRuntime, - median: medianRuntime, - }, - fixtures, - }, - } - }, {} as { [K in Output['status']]: { count: number; fixtures: string[]; comments: { unique: string[]; uniqueTemplates: string[] }; runtimes: { total: bigint; average: bigint; median: bigint } } }) - - const groupKeys = Object.keys(aggregatedGroups) as Output['status'][] - const allRuntimesSorted = groupKeys - .reduce( - (runtimes, status): bigint[] => - runtimes.concat(grouped[status].runtimes), - [] as bigint[] - ) - .sort() - - const totalCount = groupKeys.reduce( - (result, status): number => result + aggregatedGroups[status].count, - 0 - ) - const totalRuntime = groupKeys.reduce( - (result, status): bigint => - 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): bigint => - result + aggregatedGroups[status].runtimes.total, - BigInt(0) - ) - - const allComments = groupKeys.reduce( - (comments, status): Comment[] => - comments.concat(grouped[status].comments), - [] as Comment[] - ) - const allUniqueComments = [ - ...new Set( - allComments.filter(Boolean).map((comment): string => comment.message) - ), - ] - const allUniqueTemplates = [ - ...new Set( - allComments.filter(Boolean).map((comment): string => comment.template) - ), - ] - - const totalData = { - count: totalCount, - comments: { - unique: allUniqueComments, - uniqueTemplates: 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', aggregatedGroups['approve'])} -${line('Disapprove', aggregatedGroups['disapprove'])} -${line('Refer to mentor', aggregatedGroups['refer_to_mentor'])} -${line('Total', totalData)} -`.trim() - ) - }) diff --git a/src/comments/comment.ts b/src/comments/comment.ts index e4404c8c..310ad62d 100644 --- a/src/comments/comment.ts +++ b/src/comments/comment.ts @@ -1,4 +1,5 @@ -import { Comment } from '~src/interface' +/* eslint-disable @typescript-eslint/naming-convention */ +import type { Comment } from '~src/interface' type TemplateKeys = (number | string)[] type NamedTags = Record @@ -109,13 +110,13 @@ export function factory( const value = typeof key === 'number' - ? (positionalValues[key] as string) + ? positionalValues[key]! : dictionary[key as R] const tag = buildTemplateTag(key) const next = strings[i + 1] - message += (value || tag) + next + message += (value ?? tag) + next template += tag + next } @@ -155,7 +156,7 @@ function separateValues( const positionalValues = (values as (string | string[])[]) .map((item): string[] => (typeof item === 'string' ? [item] : item)) .filter(Array.isArray) - .reduce((total, array): string[] => total.concat(array), []) as string[] + .reduce((total, array): string[] => total.concat(array), []) // When there is no dictionary if ( @@ -164,7 +165,7 @@ function separateValues( Array.isArray(last) || Object.keys(last).length === 0 ) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any const dictionary: NamedTags = {} as any return { dictionary, diff --git a/src/extracts/extract_declaration.ts b/src/extracts/extract_declaration.ts index 589a952a..5fa3236c 100644 --- a/src/extracts/extract_declaration.ts +++ b/src/extracts/extract_declaration.ts @@ -1,6 +1,6 @@ import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree' -import { ExtractedFunction } from '@exercism/static-analysis' -import { Source } from '~src/analyzers/SourceImpl' +import type { ExtractedFunction } from '@exercism/static-analysis' +import type { Source } from '~src/analyzers/SourceImpl' export function extractSignature( fn: Readonly, @@ -21,7 +21,7 @@ export function extractSignature( // This will mutate const { definition } = init to const definition = init, // leaving other items inside the object- (or array-) expression alone. if (fn.node.type === AST_NODE_TYPES.VariableDeclarator) { - return `${fn.metadata.variable?.kind || 'const'} ${source.getOuter( + return `${fn.metadata.variable?.kind ?? 'const'} ${source.getOuter( signature )} ...` } diff --git a/src/extracts/extract_named_export.ts b/src/extracts/extract_named_export.ts index 2059b850..7a1ed4e6 100644 --- a/src/extracts/extract_named_export.ts +++ b/src/extracts/extract_named_export.ts @@ -1,5 +1,6 @@ -import { TSESTree } from '@typescript-eslint/typescript-estree' -import { extractExports, ExtractedExport } from '@exercism/static-analysis' +import type { TSESTree } from '@typescript-eslint/typescript-estree' +import type { ExtractedExport } from '@exercism/static-analysis' +import { extractExports } from '@exercism/static-analysis' type Node = TSESTree.Node diff --git a/src/extracts/extract_named_function.ts b/src/extracts/extract_named_function.ts index 19f9a813..f3fe5576 100644 --- a/src/extracts/extract_named_function.ts +++ b/src/extracts/extract_named_function.ts @@ -1,5 +1,6 @@ -import { TSESTree } from '@typescript-eslint/typescript-estree' -import { extractFunctions, ExtractedFunction } from '@exercism/static-analysis' +import type { TSESTree } from '@typescript-eslint/typescript-estree' +import type { ExtractedFunction } from '@exercism/static-analysis' +import { extractFunctions } from '@exercism/static-analysis' type Node = TSESTree.Node diff --git a/src/interface.d.ts b/src/interface.d.ts index 707b7b38..d6eec455 100644 --- a/src/interface.d.ts +++ b/src/interface.d.ts @@ -11,6 +11,8 @@ export interface ExecutionOptions { output: string /** The input directory path */ inputDir: string + /** The output directory path */ + outputDir: string /** The exercise slug */ exercise: string /** Unless true, expects website-copy to provide the contents of the templates */ @@ -37,7 +39,7 @@ export interface Comment { /** * The provided variables as array or name (key), value (value) map */ - variables: Readonly<{ + variables?: Readonly<{ [name: string]: string | undefined [name: number]: string | undefined }> @@ -62,26 +64,26 @@ export interface Output { * @param options the execution options * @returns the output as string */ - toProcessable(options: Readonly): Promise + toProcessable: (options: Readonly) => Promise } export interface WritableOutput extends Output { /** * @deprecated use {WritableOutput#add} + {WritableOutput#finish} */ - approve(comment?: Comment): never + approve: (comment?: Comment) => never /** * @deprecated use {WritableOutput#add} + {WritableOutput#finish} */ - disapprove(comment?: Comment): never + disapprove: (comment?: Comment) => never /** * @deprecated use {WritableOutput#add} + {WritableOutput#finish} */ - redirect(comment?: Comment): never + redirect: (comment?: Comment) => never - add(comment: Comment): void + add: (comment: Comment) => void - finish(summary?: string): never + finish: (summary?: string) => never hasCommentary: boolean commentCount: number @@ -95,13 +97,13 @@ export interface OutputProcessor { } export interface Analyzer { - run(input: Input, options: ExecutionOptions): Promise + run: (input: Input, options: ExecutionOptions) => Promise } export interface Runner { - call( + call: ( analyzer: Analyzer, input: Input, options: Readonly - ): Promise + ) => Promise } diff --git a/src/markdownify.ts b/src/markdownify.ts index 30f01c73..51984003 100644 --- a/src/markdownify.ts +++ b/src/markdownify.ts @@ -1,7 +1,7 @@ import { readFile } from '@exercism/static-analysis' import { spawnSync } from 'child_process' import path from 'path' -import { Output } from './interface' +import type { Output } from './interface' import { Bootstrap } from './utils/bootstrap' // The bootstrap call uses the arguments passed to the process to figure out @@ -19,13 +19,11 @@ logger.log('=> DEBUG mode is on') readFile(path.join(options.inputDir, 'analysis.json')) .then( - (jsonString: Buffer | string): Pick => - JSON.parse(jsonString.toString()) + (jsonString: Buffer | string): Pick => + JSON.parse(jsonString.toString()) as Pick ) - .then((output: Pick): void => { - logger.log( - `=> Got ${output.status} with ${output.comments.length} comments` - ) + .then((output: Pick): void => { + logger.log(`=> Got ${output.comments.length} comments`) const spawned = spawnSync( 'ruby', [ @@ -48,16 +46,14 @@ readFile(path.join(options.inputDir, 'analysis.json')) } ) - spawned.error && logger.log(spawned.error.toString()) - if (spawned.output) { - const [, out, err] = spawned.output - if (out) { - logger.log(out.toString().trim()) - } + spawned.error && logger.log(spawned.error.message) + const [, out, err] = spawned.output + if (out) { + logger.log(out.toString().trim()) + } - if (err) { - logger.error(err.toString().trim()) - } + if (err) { + logger.error(err.toString().trim()) } }) - .catch((err): void => logger.error(err)) + .catch((err): void => void logger.error(err)) diff --git a/src/output/AnalyzerOutput.ts b/src/output/AnalyzerOutput.ts index ede305ee..b884c572 100644 --- a/src/output/AnalyzerOutput.ts +++ b/src/output/AnalyzerOutput.ts @@ -1,30 +1,5 @@ import type { Comment, ExecutionOptions, Output } from '~src/interface' -enum SolutionStatus { - /** - * This is the default situation and should be used when there is any - * uncertainty. - * - * @deprecated don't return any status or use an {Essential} comment. - * */ - Redirect = 'refer_to_mentor', - /** - * To be used when a solution matches pre-known optimal solutions or when a - * solution can be approved but with a known improvement. - * - * @deprecated don't return any status or use a {Celebratory} comment. - * */ - Approve = 'approve', - /** - * To be used when a solution can be disapproved as suboptimal and a comment - * is provided. - * - * @deprecated replace with one or more comments with {Essential} or an - * {Actionable} type. - **/ - Disapprove = 'disapprove', -} - /** * The interface for the analyzer output is described [here][doc]. * @@ -77,7 +52,7 @@ export class AnalyzerOutput implements Output { } public freeze(summary?: string): void { - this.summary = summary || this.summary + this.summary = summary ?? this.summary Object.freeze(this) Object.freeze(this.comments) diff --git a/src/output/IsolatedAnalyzerOutput.ts b/src/output/IsolatedAnalyzerOutput.ts index ec338b9e..cfb81221 100644 --- a/src/output/IsolatedAnalyzerOutput.ts +++ b/src/output/IsolatedAnalyzerOutput.ts @@ -44,7 +44,7 @@ export class IsolatedAnalyzerOutput } public freeze(summary?: string): never { - this.summary = summary || this.summary + this.summary = summary ?? this.summary super.freeze() throw new EarlyFinalization() diff --git a/src/output/makeParseErrorOutput.ts b/src/output/makeParseErrorOutput.ts index 47f1bec3..27c626a6 100644 --- a/src/output/makeParseErrorOutput.ts +++ b/src/output/makeParseErrorOutput.ts @@ -15,7 +15,7 @@ export function makeParseErrorOutput(err: ParserError): Output { const output = new AnalyzerOutput() const { message, ...details } = err.original - const source = new Source(err.source || '') + const source = new Source(err.source ?? '') const startLine = details.lineNumber - 2 const endLine = details.lineNumber + 3 /* last line might be empty */ diff --git a/src/output/processor/FileOutput.ts b/src/output/processor/FileOutput.ts index 9c0471d9..f5361630 100644 --- a/src/output/processor/FileOutput.ts +++ b/src/output/processor/FileOutput.ts @@ -1,10 +1,11 @@ import { getProcessLogger, writeFile } from '@exercism/static-analysis' import path from 'path' -import { ExecutionOptions, OutputProcessor } from '~src/interface' +import type { ExecutionOptions, OutputProcessor } from '~src/interface' -type FileOutputOptions = Pick +type FileOutputOptions = Pick +// eslint-disable-next-line @typescript-eslint/naming-convention export const FileOutput: OutputProcessor = async ( previous: Promise, options: FileOutputOptions @@ -16,6 +17,6 @@ export const FileOutput: OutputProcessor = async ( return writeFile(outputPath, output).then(() => output) } -function getOutputPath({ output, inputDir }: FileOutputOptions): string { - return path.isAbsolute(output) ? output : path.join(inputDir, output) +function getOutputPath({ output, outputDir }: FileOutputOptions): string { + return path.isAbsolute(output) ? output : path.join(outputDir, output) } diff --git a/src/output/processor/LogOutput.ts b/src/output/processor/LogOutput.ts index 14d5ae52..3a189dcf 100644 --- a/src/output/processor/LogOutput.ts +++ b/src/output/processor/LogOutput.ts @@ -1,6 +1,7 @@ import { getProcessLogger } from '@exercism/static-analysis' import type { OutputProcessor } from '~src/interface' +// eslint-disable-next-line @typescript-eslint/naming-convention export const LogOutput: OutputProcessor = async ( previous: Promise ): Promise => { diff --git a/src/output/processor/PassThroughOutput.ts b/src/output/processor/PassThroughOutput.ts index 5951c16b..3b5c0de9 100644 --- a/src/output/processor/PassThroughOutput.ts +++ b/src/output/processor/PassThroughOutput.ts @@ -1,5 +1,6 @@ -import { OutputProcessor } from '~src/interface' +import type { OutputProcessor } from '~src/interface' +// eslint-disable-next-line @typescript-eslint/naming-convention export const PassThroughOutput: OutputProcessor = async ( previous: Promise ): Promise => { diff --git a/src/remote-analyze.ts b/src/remote-analyze.ts index 2d6cd98a..40511b28 100644 --- a/src/remote-analyze.ts +++ b/src/remote-analyze.ts @@ -35,13 +35,13 @@ const input = options.inputDir.trim() let uuid = undefined if (input.startsWith('https://exercism.io/')) { uuid = input.split('/').reverse()[0] - if (uuid.length != 32) { + if (uuid.length !== 32) { process.stderr.write( `Expected a UUID (length 32), got '${uuid}' (len: ${uuid.length})` ) process.exit(-2) } -} else if (input.length == 32) { +} else if (input.length === 32) { uuid = input } else if (fs.existsSync(input)) { logger.error('=> input seems to be local') @@ -101,14 +101,14 @@ const analyzeProcess = spawn( { cwd: process.cwd(), env: process.env } ) -analyzeProcess.stderr.on('data', (data) => { - logger.error(data.toString().trim()) +analyzeProcess.stderr.on('data', (data: unknown) => { + logger.error(`${data}`.trim()) }) -analyzeProcess.stdout.on('data', (data) => { - logger.log(data.toString().trim()) +analyzeProcess.stdout.on('data', (data: unknown) => { + logger.log(`${data}`.trim()) }) analyzeProcess.on('close', (code) => { - process.exit(code || undefined) + process.exit(code ?? undefined) }) diff --git a/src/stats.ts b/src/stats.ts deleted file mode 100644 index 10c31e7b..00000000 --- a/src/stats.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { AstParser } from '@exercism/static-analysis/dist/AstParser' -import { DirectoryInput } from '@exercism/static-analysis/dist/input/DirectoryInput' -import { readDir } from '@exercism/static-analysis/dist/utils/fs' -import path from 'path' -import { Bootstrap } from './utils/bootstrap' - -// 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 = ' '): string { - return (pad + value).slice(-pad.length) -} - -logger.log(`=> start statistics collection for ${exercise.slug}`) - -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 AstParser.ANALYZER.parse(input) - - if (results.length === 0) { - throw new Error(`No input source files for ${exercise.slug}`) - } - - const [{ program: root }] = results - - 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 readonly 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() - ) - }) diff --git a/src/utils/bootstrap.ts b/src/utils/bootstrap.ts index 530c2cad..a4572430 100644 --- a/src/utils/bootstrap.ts +++ b/src/utils/bootstrap.ts @@ -1,13 +1,13 @@ +import type { Input } from '@exercism/static-analysis' import { DirectoryInput, - Input, Logger, registerExceptionHandler, setProcessLogger, DirectoryWithConfigInput, } from '@exercism/static-analysis' import { ExerciseImpl } from '~src/ExerciseImpl' -import { ExecutionOptions, Exercise } from '~src/interface' +import type { ExecutionOptions, Exercise } from '~src/interface' import { ExecutionOptionsImpl } from './execution_options' export interface BootstrapResult { @@ -37,6 +37,7 @@ export class Bootstrap { registerExceptionHandler() const options = ExecutionOptionsImpl.create() + const logger = new Logger(options) const exercise = new ExerciseImpl(options.exercise) const input = DirectoryWithConfigInput.test(options.inputDir) diff --git a/src/utils/execution_options.ts b/src/utils/execution_options.ts index acf9f617..2ffcf75c 100644 --- a/src/utils/execution_options.ts +++ b/src/utils/execution_options.ts @@ -5,6 +5,7 @@ export class ExecutionOptionsImpl implements ExecutionOptions { public debug!: boolean public console!: boolean public output!: string + public outputDir!: string public inputDir!: string public exercise!: string public dry!: boolean @@ -17,7 +18,9 @@ export class ExecutionOptionsImpl implements ExecutionOptions { public static create(): ExecutionOptions { const args = yargs - .usage('Usage: $0 [options]') + .usage( + 'Usage: $0 [] [options]' + ) .example( '$0 two-fer ~/javascript/two-fer/128/', 'Analyze the input directory "128" against the two-fer analyzer' @@ -30,7 +33,7 @@ export class ExecutionOptionsImpl implements ExecutionOptions { .describe('c', 'If given, outputs to the console') .describe( 'o', - 'Path relative to the input dir where the analyzis results are stored' + 'Path relative to the input dir where the analysis results are stored' ) .describe( 'noTemplates', @@ -62,6 +65,7 @@ export class ExecutionOptionsImpl implements ExecutionOptions { noTemplates, exercise: String(_[0]), inputDir: String(_[1]), + outputDir: String(_[2] || _[1]), }) } } diff --git a/src/web.ts b/src/web.ts index 56018cf3..b03076cc 100644 --- a/src/web.ts +++ b/src/web.ts @@ -32,7 +32,8 @@ async function internalRun( ): Promise { // 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) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const analysis = await analyzer.run(input, {} as any) // An output processor gets the Promise to the previous output processor and // can add its own side-effects or transformation. @@ -77,7 +78,10 @@ export async function run( ) return await internalRun(analyzer, input, options) - } catch (err) { - reportException(err) + } catch (err: unknown) { + if (err instanceof Error) { + reportException(err) + } + return Promise.reject(err) } } diff --git a/test/.eslintrc b/test/.eslintrc deleted file mode 100644 index 83fe3eeb..00000000 --- a/test/.eslintrc +++ /dev/null @@ -1,17 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./test/tsconfig.eslint.json", - "ecmaVersion": 2018, - "sourceType": "module", - "ecmaFeatures": { - "modules": true - } - }, - "env": { - "es6": true, - "node": true, - "jest": true - }, - "extends": ["../.eslintrc"] -} diff --git a/test/__mocks__/fs.ts b/test/__mocks__/fs.ts deleted file mode 100644 index ad0e01b2..00000000 --- a/test/__mocks__/fs.ts +++ /dev/null @@ -1,263 +0,0 @@ -import path from 'path' -import { - BaseEncodingOptions, - Dirent, - Mode, - OpenMode, - PathLike, - Stats, -} from 'fs' -import type { promises } from 'fs' -type FileHandle = promises.FileHandle - -const fs = jest.createMockFromModule('fs') as Omit< - typeof import('fs'), - never -> & { - __setMockFiles: typeof __setMockFiles - __getWrittenFiles: typeof __getWrittenFiles -} - -fs.promises = {} as typeof fs.promises - -// This is a custom function that our tests can use during setup to specify -// what the files on the "mock" filesystem should look like when any of the -// `fs` APIs are used. -let mockFiles: { [dir: string]: { [file: string]: string } } = Object.create( - null -) -let writtenFiles: { [dir: string]: { [file: string]: string } } = Object.create( - null -) - -class NotMocked extends Error { - public readonly code: string - public readonly errno: 34 - - constructor(key: string) { - super( - `${key} does not exist (because it has not been mocked)\n` + - `Mocked are: \n${JSON.stringify(mockFiles, null, 2)}` - ) - - this.code = 'ENOENT' - this.errno = 34 - - Error.captureStackTrace(this, this.constructor) - } -} - -class CanOnlyWriteUnmockedFiles extends Error { - public readonly code: string - public readonly errno: 47 - - constructor(key: string) { - super(`${key} already exists as readonly (because it has been mocked)`) - - this.code = 'EEXIST' - this.errno = 47 - - Error.captureStackTrace(this, this.constructor) - } -} - -function __setMockFiles(newMockFiles: { [path: string]: string }): void { - mockFiles = Object.create(null) - writtenFiles = Object.create(null) - - for (const fullPath in newMockFiles) { - const osPath = path.normalize(fullPath) - const dir = path.dirname(osPath) - const file = path.basename(osPath) - - if (!mockFiles[dir]) { - mockFiles[dir] = {} - } - - mockFiles[dir][file] = newMockFiles[fullPath] - } -} - -function __getWrittenFiles(): typeof writtenFiles { - return JSON.parse(JSON.stringify(writtenFiles)) -} - -/** - * Asynchronous readdir(3) - read a directory. - * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. - * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. - */ -function readdir( - path: PathLike, - options?: - | (BaseEncodingOptions & { withFileTypes?: false }) - | BufferEncoding - | null -): Promise - -/** - * Asynchronous readdir(3) - read a directory. - * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. - * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. - */ -function readdir( - path: PathLike, - options: { encoding: 'buffer'; withFileTypes?: false } | 'buffer' -): Promise - -/** - * Asynchronous readdir(3) - read a directory. - * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. - * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used. - */ -function readdir( - path: PathLike, - options?: - | (BaseEncodingOptions & { withFileTypes?: false }) - | BufferEncoding - | null -): Promise - -/** - * Asynchronous readdir(3) - read a directory. - * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. - * @param options If called with `withFileTypes: true` the result data will be an array of Dirent. - */ -function readdir( - path: PathLike, - options: BaseEncodingOptions & { withFileTypes: true } -): Promise - -async function readdir( - dirPath: PathLike -): Promise { - const key = path.normalize(dirPath.toString()) - - if (key in mockFiles) { - return Promise.resolve(Object.keys(mockFiles[key])) - } - - throw new NotMocked(key) -} - -/** - * Asynchronously reads the entire contents of a file. - * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. - * If a `FileHandle` is provided, the underlying file will _not_ be closed automatically. - * @param options An object that may contain an optional flag. - * If a flag is not provided, it defaults to `'r'`. - */ -function readFile( - path: PathLike | FileHandle, - options?: { encoding?: null; flag?: OpenMode } | null -): Promise - -/** - * Asynchronously reads the entire contents of a file. - * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. - * If a `FileHandle` is provided, the underlying file will _not_ be closed automatically. - * @param options An object that may contain an optional flag. - * If a flag is not provided, it defaults to `'r'`. - */ -function readFile( - path: PathLike | FileHandle, - options: { encoding: BufferEncoding; flag?: OpenMode } | BufferEncoding -): Promise - -/** - * Asynchronously reads the entire contents of a file. - * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. - * If a `FileHandle` is provided, the underlying file will _not_ be closed automatically. - * @param options An object that may contain an optional flag. - * If a flag is not provided, it defaults to `'r'`. - */ -function readFile( - path: PathLike | FileHandle, - options?: (BaseEncodingOptions & { flag?: OpenMode }) | BufferEncoding | null -): Promise - -async function readFile( - filePath: PathLike | FileHandle -): Promise { - const key = path.normalize(filePath.toString()) - const dir = path.dirname(key) - const file = path.basename(key) - - if (dir in mockFiles && file in mockFiles[dir]) { - return Buffer.from(mockFiles[dir][file]) - } - - throw new NotMocked(key) -} - -/** - * Asynchronously writes data to a file, replacing the file if it already exists. - * It is unsafe to call `fsPromises.writeFile()` multiple times on the same file without waiting for the `Promise` to be resolved (or rejected). - * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. - * URL support is _experimental_. - * If a `FileHandle` is provided, the underlying file will _not_ be closed automatically. - * @param data The data to write. If something other than a `Buffer` or `Uint8Array` is provided, the value is coerced to a string. - * @param options Either the encoding for the file, or an object optionally specifying the encoding, file mode, and flag. - * If `encoding` is not supplied, the default of `'utf8'` is used. - * If `mode` is not supplied, the default of `0o666` is used. - * If `mode` is a string, it is parsed as an octal integer. - * If `flag` is not supplied, the default of `'w'` is used. - */ -function writeFile( - path: PathLike | FileHandle, - data: string | Uint8Array, - options?: - | (BaseEncodingOptions & { mode?: Mode; flag?: OpenMode }) - | BufferEncoding - | null -): Promise - -async function writeFile( - filePath: PathLike | FileHandle, - data: string | Uint8Array -): Promise { - const key = path.normalize(filePath.toString()) - const dir = path.dirname(key) - const file = path.basename(key) - - if (dir in mockFiles && file in mockFiles[dir]) { - throw new CanOnlyWriteUnmockedFiles(key) - } - - if (!writtenFiles[dir]) { - writtenFiles[dir] = {} - } - - writtenFiles[dir][file] = String(data) -} - -/** - * Asynchronous stat(2) - Get file status. - * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. - */ -function stat(path: PathLike): Promise - -async function stat(filePath: PathLike): Promise { - const key = path.normalize(filePath.toString()) - const dir = path.dirname(key) - const file = path.basename(key) - - const exists = - key in mockFiles || (dir in mockFiles && file in mockFiles[dir]) - - if (exists) { - return {} as Stats - } - - throw new NotMocked(key) -} - -fs.__getWrittenFiles = __getWrittenFiles -fs.__setMockFiles = __setMockFiles - -fs.promises.readdir = readdir -fs.promises.readFile = readFile -fs.promises.writeFile = writeFile -fs.promises.stat = stat - -export default fs diff --git a/test/comments/parameters.ts b/test/comments/parameters.ts index 6000fd61..2923578d 100644 --- a/test/comments/parameters.ts +++ b/test/comments/parameters.ts @@ -63,142 +63,145 @@ with some indentation it('has an empty set of variables if none are passed', () => { expect(comment.variables).toEqual({}) }) - }), - describe('when passing all named parameters', () => { - const comment = parametarable({ + }) + + describe('when passing all named parameters', () => { + const comment = parametarable({ + foo: 'actual-foo', + bar: 'actual-bar', + }) + + it('generates the message', () => { + expect(comment.message).toBe( + [ + 'This is a message with parameters:', + '---', + 'foo => actual-foo.', + ' positional => [%0$s, %1$s, %2$s]', + ' bar => actual-bar', + ' foo (again) => actual-foo', + 'with some indentation', + ].join('\n') + ) + }) + + it('assigns the external template identifier', () => { + expect(comment.externalTemplate).toBe('test.javascript.parameters') + }) + + it('gives the original template with template variables', () => { + expect(comment.template).toBe( + [ + 'This is a message with parameters:', + '---', + 'foo => %{foo}.', + ' positional => [%0$s, %1$s, %2$s]', + ' bar => %{bar}', + ' foo (again) => %{foo}', + 'with some indentation', + ].join('\n') + ) + }) + + it('has the set of variables passed', () => { + expect(comment.variables).toEqual({ foo: 'actual-foo', bar: 'actual-bar', }) + }) + }) - it('generates the message', () => { - expect(comment.message).toBe( - [ - 'This is a message with parameters:', - '---', - 'foo => actual-foo.', - ' positional => [%0$s, %1$s, %2$s]', - ' bar => actual-bar', - ' foo (again) => actual-foo', - 'with some indentation', - ].join('\n') - ) - }) - - it('assigns the external template identifier', () => { - expect(comment.externalTemplate).toBe('test.javascript.parameters') - }) + describe('when passing some positional parameters', () => { + const comment = parametarable('actual-foo', 'actual-bar') - it('gives the original template with template variables', () => { - expect(comment.template).toBe( - [ - 'This is a message with parameters:', - '---', - 'foo => %{foo}.', - ' positional => [%0$s, %1$s, %2$s]', - ' bar => %{bar}', - ' foo (again) => %{foo}', - 'with some indentation', - ].join('\n') - ) - }) + it('generates the message', () => { + expect(comment.message).toBe( + [ + 'This is a message with parameters:', + '---', + 'foo => %{foo}.', + ' positional => [actual-foo, actual-bar, %2$s]', + ' bar => %{bar}', + ' foo (again) => %{foo}', + 'with some indentation', + ].join('\n') + ) + }) - it('has the set of variables passed', () => { - expect(comment.variables).toEqual({ - foo: 'actual-foo', - bar: 'actual-bar', - }) - }) - }), - describe('when passing some positional parameters', () => { - const comment = parametarable('actual-foo', 'actual-bar') - - it('generates the message', () => { - expect(comment.message).toBe( - [ - 'This is a message with parameters:', - '---', - 'foo => %{foo}.', - ' positional => [actual-foo, actual-bar, %2$s]', - ' bar => %{bar}', - ' foo (again) => %{foo}', - 'with some indentation', - ].join('\n') - ) - }) + it('assigns the external template identifier', () => { + expect(comment.externalTemplate).toBe('test.javascript.parameters') + }) - it('assigns the external template identifier', () => { - expect(comment.externalTemplate).toBe('test.javascript.parameters') - }) + it('gives the original template with template variables', () => { + expect(comment.template).toBe( + [ + 'This is a message with parameters:', + '---', + 'foo => %{foo}.', + ' positional => [%0$s, %1$s, %2$s]', + ' bar => %{bar}', + ' foo (again) => %{foo}', + 'with some indentation', + ].join('\n') + ) + }) - it('gives the original template with template variables', () => { - expect(comment.template).toBe( - [ - 'This is a message with parameters:', - '---', - 'foo => %{foo}.', - ' positional => [%0$s, %1$s, %2$s]', - ' bar => %{bar}', - ' foo (again) => %{foo}', - 'with some indentation', - ].join('\n') - ) + it('has the array of positional parameters', () => { + expect(comment.variables).toEqual({ + 0: 'actual-foo', + 1: 'actual-bar', }) + }) + }) - it('has the array of positional parameters', () => { - expect(comment.variables).toEqual({ - 0: 'actual-foo', - 1: 'actual-bar', - }) - }) - }), - describe('when passing all parameters', () => { - const comment = parametarable(['posi-foo', 'posi-bar', 'posi-baz'], { - foo: 'name-foo', - bar: 'name-bar', - }) + describe('when passing all parameters', () => { + const comment = parametarable(['posi-foo', 'posi-bar', 'posi-baz'], { + foo: 'name-foo', + bar: 'name-bar', + }) - it('generates the message', () => { - expect(comment.message).toBe( - [ - 'This is a message with parameters:', - '---', - 'foo => name-foo.', - ' positional => [posi-foo, posi-bar, posi-baz]', - ' bar => name-bar', - ' foo (again) => name-foo', - 'with some indentation', - ].join('\n') - ) - }) + it('generates the message', () => { + expect(comment.message).toBe( + [ + 'This is a message with parameters:', + '---', + 'foo => name-foo.', + ' positional => [posi-foo, posi-bar, posi-baz]', + ' bar => name-bar', + ' foo (again) => name-foo', + 'with some indentation', + ].join('\n') + ) + }) - it('assigns the external template identifier', () => { - expect(comment.externalTemplate).toBe('test.javascript.parameters') - }) + it('assigns the external template identifier', () => { + expect(comment.externalTemplate).toBe('test.javascript.parameters') + }) - it('gives the original template with template variables', () => { - expect(comment.template).toBe( - [ - 'This is a message with parameters:', - '---', - 'foo => %{foo}.', - ' positional => [%0$s, %1$s, %2$s]', - ' bar => %{bar}', - ' foo (again) => %{foo}', - 'with some indentation', - ].join('\n') - ) - }) + it('gives the original template with template variables', () => { + expect(comment.template).toBe( + [ + 'This is a message with parameters:', + '---', + 'foo => %{foo}.', + ' positional => [%0$s, %1$s, %2$s]', + ' bar => %{bar}', + ' foo (again) => %{foo}', + 'with some indentation', + ].join('\n') + ) + }) - it('has the array of positional parameters', () => { - expect(comment.variables).toEqual({ - 0: 'posi-foo', - 1: 'posi-bar', - 2: 'posi-baz', - foo: 'name-foo', - bar: 'name-bar', - }) + it('has the array of positional parameters', () => { + expect(comment.variables).toEqual({ + 0: 'posi-foo', + 1: 'posi-bar', + 2: 'posi-baz', + foo: 'name-foo', + bar: 'name-bar', }) }) + }) }) }) }) diff --git a/test/fixtures/bird-watcher/bird-watcher.js b/test/fixtures/bird-watcher/bird-watcher.js new file mode 100644 index 00000000..1086ea7e --- /dev/null +++ b/test/fixtures/bird-watcher/bird-watcher.js @@ -0,0 +1,49 @@ +// @ts-check +// +// The line above enables type checking for this file. Various IDEs interpret +// the @ts-check directive. It will give you helpful autocompletion when +// implementing this exercise. + +/** + * Calculates the total bird count. + * + * @param {number[]} birdsPerDay + * @returns {number} total bird count + */ +export function totalBirdCount(birdsPerDay) { + let total = 0; + for (let i = 0; i < birdsPerDay.length; i++) { + total += birdsPerDay[i]; + } + return total; +} + +/** + * Calculates the total number of birds seen in a specific week. + * + * @param {number[]} birdsPerDay + * @param {number} week + * @returns {number} birds counted in the given week + */ +export function birdsInWeek(birdsPerDay, week) { + let total = 0; + const start = 7 * (week - 1); + for (let i = start; i < start + 7; i++) { + total += birdsPerDay[i]; + } + return total; +} + +/** + * Fixes the counting mistake by increasing the bird count + * by one for every second day. + * + * @param {number[]} birdsPerDay + * @returns {number[]} corrected bird count data + */ +export function fixBirdCountLog(birdsPerDay) { + for (let i = 0; i < birdsPerDay.length; i += 2) { + birdsPerDay[i]++; + } + return birdsPerDay; +} \ No newline at end of file diff --git a/test/fixtures/bird-watcher/bird-watcher.spec.js b/test/fixtures/bird-watcher/bird-watcher.spec.js new file mode 100644 index 00000000..13c6c0b0 --- /dev/null +++ b/test/fixtures/bird-watcher/bird-watcher.spec.js @@ -0,0 +1,80 @@ +import { totalBirdCount, birdsInWeek, fixBirdCountLog } from './bird-watcher'; + +describe('bird watcher', () => { + describe('totalBirdCount', () => { + xtest('calculates the correct total number of birds', () => { + const birdsPerDay = [9, 0, 8, 4, 5, 1, 3]; + expect(totalBirdCount(birdsPerDay)).toBe(30); + }); + + xtest('works for a short bird count list', () => { + const birdsPerDay = [2]; + expect(totalBirdCount(birdsPerDay)).toBe(2); + }); + + xtest('works for a long bird count list', () => { + // prettier-ignore + const birdsPerDay = [2, 8, 4, 1, 3, 5, 0, 4, 1, 6, 0, 3, 0, 1, 5, 4, 1, 1, 2, 6]; + expect(totalBirdCount(birdsPerDay)).toBe(57); + }); + }); + + describe('birdsInWeek', () => { + xtest('calculates the number of birds in the first week', () => { + const birdsPerDay = [3, 0, 5, 1, 0, 4, 1, 0, 3, 4, 3, 0, 8, 0]; + expect(birdsInWeek(birdsPerDay, 1)).toBe(14); + }); + + xtest('calculates the number of birds for a week in the middle of the log', () => { + // prettier-ignore + const birdsPerDay = [4, 7, 3, 2, 1, 1, 2, 0, 2, 3, 2, 7, 1, 3, 0, 6, 5, 3, 7, 2, 3]; + expect(birdsInWeek(birdsPerDay, 2)).toBe(18); + }); + + xtest('works when there is only one week', () => { + const birdsPerDay = [3, 0, 3, 3, 2, 1, 0]; + expect(birdsInWeek(birdsPerDay, 1)).toBe(12); + }); + + xtest('works for a long bird count list', () => { + const week21 = [2, 0, 1, 4, 1, 3, 0]; + const birdsPerDay = randomArray(20 * 7) + .concat(week21) + .concat(randomArray(10 * 7)); + + expect(birdsInWeek(birdsPerDay, 21)).toBe(11); + }); + }); + + describe('fixBirdCountLog', () => { + xtest('returns a bird count list with the corrected values', () => { + const birdsPerDay = [3, 0, 5, 1, 0, 4, 1, 0, 3, 4, 3, 0]; + const expected = [4, 0, 6, 1, 1, 4, 2, 0, 4, 4, 4, 0]; + expect(fixBirdCountLog(birdsPerDay)).toEqual(expected); + }); + + xtest('does not create a new array', () => { + const birdsPerDay = [2, 0, 1, 4, 1, 3, 0]; + + // this follows the suggestion from the Jest docs to avoid a confusing test report + // https://jestjs.io/docs/expect#tobevalue + expect(Object.is(fixBirdCountLog(birdsPerDay), birdsPerDay)).toBe(true); + }); + + xtest('works for a short bird count list', () => { + expect(fixBirdCountLog([4, 2])).toEqual([5, 2]); + }); + + xtest('works for a long bird count list', () => { + // prettier-ignore + const birdsPerDay = [2, 8, 4, 1, 3, 5, 0, 4, 1, 6, 0, 3, 0, 1, 5, 4, 1, 1, 2, 6]; + // prettier-ignore + const expected = [3, 8, 5, 1, 4, 5, 1, 4, 2, 6, 1, 3, 1, 1, 6, 4, 2, 1, 3, 6]; + expect(fixBirdCountLog(birdsPerDay)).toEqual(expected); + }); + }); +}); + +function randomArray(length) { + return Array.from({ length: length }, () => Math.floor(Math.random() * 8)); +} \ No newline at end of file diff --git a/test/helpers/bootstrap.ts b/test/helpers/bootstrap.ts index 3a3331f3..4c109ffc 100644 --- a/test/helpers/bootstrap.ts +++ b/test/helpers/bootstrap.ts @@ -1,7 +1,7 @@ import { ExecutionOptionsImpl } from '~src/utils/execution_options' import { ExerciseImpl } from '~src/ExerciseImpl' -import { BootstrapResult } from '~src/utils/bootstrap' -import { ExecutionOptions } from '~src/interface' +import type { BootstrapResult } from '~src/utils/bootstrap' +import type { ExecutionOptions } from '~src/interface' import { Logger, setProcessLogger } from '@exercism/static-analysis' export function bootstrap({ diff --git a/test/helpers/smoke.ts b/test/helpers/smoke.ts index 849b8306..82224878 100644 --- a/test/helpers/smoke.ts +++ b/test/helpers/smoke.ts @@ -1,8 +1,8 @@ import { InlineInput } from '@exercism/static-analysis' -import { Analyzer, ExecutionOptions, Output } from '~src/interface' +import type { Analyzer, ExecutionOptions, Output } from '~src/interface' type AnalyzerFactory = () => Analyzer -type analyze = (solutionContent: string) => Promise +type Analyze = (solutionContent: string) => Promise const EMPTY_OPTIONS: Omit< ExecutionOptions, @@ -23,12 +23,13 @@ export function makeOptions( } export function makeAnalyze( + // eslint-disable-next-line @typescript-eslint/naming-convention AnalyzerFactory: AnalyzerFactory, options: Omit< ExecutionOptions, 'exercise' | 'inputDir' | 'output' > = EMPTY_OPTIONS -): analyze { +): Analyze { return async function (solutionContent: string): Promise { const analyzer = AnalyzerFactory() const input = new InlineInput([solutionContent]) diff --git a/test/helpers/snapshot.ts b/test/helpers/snapshot.ts index 2dabb7e1..01d7ccd4 100644 --- a/test/helpers/snapshot.ts +++ b/test/helpers/snapshot.ts @@ -1,4 +1,4 @@ -import { Analyzer, ExecutionOptions, Output } from '~src/interface' +import type { Analyzer, ExecutionOptions, Output } from '~src/interface' import { FixtureInput } from './input/FixtureInput' const EMPTY_OPTIONS: ExecutionOptions = { @@ -13,12 +13,13 @@ const EMPTY_OPTIONS: ExecutionOptions = { } type AnalyzerFactory = () => Analyzer -type generateAll = (fixtures: readonly number[]) => void +type GenerateAll = (fixtures: readonly number[]) => void export function makeTestGenerator( slug: string, + // eslint-disable-next-line @typescript-eslint/naming-convention AnalyzerFactory: AnalyzerFactory -): generateAll { +): GenerateAll { function analyze(fixture: number): Promise { const analyzer = AnalyzerFactory() const input = new FixtureInput(slug, fixture) @@ -30,7 +31,7 @@ export function makeTestGenerator( describe(`and expecting`, () => { fixtures .slice() - .sort() + .sort((a, b) => a - b) .forEach((fixture) => { const identifier = `${slug}/${fixture}` it(`matches ${identifier}'s output`, async () => { diff --git a/test/output/processor/FileOutput.ts b/test/output/processor/FileOutput.ts deleted file mode 100644 index 87d65a4b..00000000 --- a/test/output/processor/FileOutput.ts +++ /dev/null @@ -1,148 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { ExecutionOptions } from '~src/interface' -import { FileOutput } from '~src/output/processor/FileOutput' - -const mockedFs = (fs as unknown) as MockedFs - -// jest.mock('fs') - -function mockFiles(files: { [path: string]: string }): void { - mockedFs.__setMockFiles(files) -} - -function getWrittenFiles(): { [dir: string]: { [file: string]: string } } { - return mockedFs.__getWrittenFiles() -} - -const CONTENTS = `My Fine Output` -const DEFAULT_OPTIONS: Omit = { - debug: false, - dry: false, - console: false, - exercise: '', - noTemplates: false, - pretty: false, -} - -// TODO: Mock seems broken, so skip this for now -describe.skip('FileOutput', () => { - describe('when the output path is writable', () => { - const OUT_OPTIONS: ExecutionOptions = { - inputDir: '/path/to/input', - output: 'analysis.out', - ...DEFAULT_OPTIONS, - } - - beforeEach(() => { - mockFiles({}) - }) - - it("doesn't modify the previous stream", async () => { - const processed = await FileOutput(Promise.resolve(CONTENTS), OUT_OPTIONS) - expect(processed).toStrictEqual(CONTENTS) - }) - - it('writes the processable stream to disk', async () => { - await FileOutput(Promise.resolve(CONTENTS), OUT_OPTIONS) - const files = getWrittenFiles() - expect(files).toMatchObject({ - [path.normalize(OUT_OPTIONS.inputDir)]: { - [OUT_OPTIONS.output]: CONTENTS, - }, - }) - }) - }) - - describe('when the output path is not writable', () => { - const OUT_OPTIONS: ExecutionOptions = { - ...DEFAULT_OPTIONS, - inputDir: '/path/to/input', - output: 'analysis.out', - } - - beforeEach(() => { - mockFiles({ - [path.join( - OUT_OPTIONS.inputDir, - OUT_OPTIONS.output - )]: 'Already Written', - }) - }) - - it('bubbles the unwritable error', async () => { - return expect( - FileOutput(Promise.resolve(CONTENTS), OUT_OPTIONS) - ).rejects.toMatchObject({ - errno: 47, - code: 'EEXIST', - }) - }) - }) - - describe('when the output is absolute', () => { - const OUT_OPTIONS: ExecutionOptions = { - ...DEFAULT_OPTIONS, - inputDir: '/not', - output: '/path/to/output/analysis.out', - } - - beforeEach(() => { - mockFiles({}) - }) - - it('uses the output path only', async () => { - await FileOutput(Promise.resolve(CONTENTS), OUT_OPTIONS) - const files = getWrittenFiles() - - const outFile = path.normalize(path.basename(OUT_OPTIONS.output)) - - // Written at output - expect(files).toMatchObject({ - [path.normalize(path.dirname(OUT_OPTIONS.output))]: { - [outFile]: CONTENTS, - }, - }) - - // Not written at not/path/to/output - expect(files).not.toMatchObject({ - [path.normalize(OUT_OPTIONS.inputDir)]: { - [outFile]: CONTENTS, - }, - }) - - // Not written at combination of the two - expect(files).not.toMatchObject({ - [path.normalize( - path.join(OUT_OPTIONS.inputDir, path.dirname(OUT_OPTIONS.output)) - )]: { - [outFile]: CONTENTS, - }, - }) - }) - }) - - describe('when the output is relative', () => { - const OUT_OPTIONS: ExecutionOptions = { - ...DEFAULT_OPTIONS, - inputDir: '/path/to/input', - output: 'analysis.out', - } - - beforeEach(() => { - mockFiles({}) - }) - - it('uses the input dir to write the output file', async () => { - await FileOutput(Promise.resolve(CONTENTS), OUT_OPTIONS) - const files = getWrittenFiles() - - // Written at input dir - expect(files).toMatchObject({ - [path.normalize(OUT_OPTIONS.inputDir)]: { - [OUT_OPTIONS.output]: CONTENTS, - }, - }) - }) - }) -}) diff --git a/test/output/processor/LogOutput.ts b/test/output/processor/LogOutput.ts index 8247be02..f331bac1 100644 --- a/test/output/processor/LogOutput.ts +++ b/test/output/processor/LogOutput.ts @@ -1,18 +1,17 @@ import { LogOutput } from '~src/output/processor/LogOutput' -import { ExecutionOptions } from '~src/interface' -import { - Logger, - LoggerInput, - setProcessLogger, -} from '@exercism/static-analysis' +import type { ExecutionOptions } from '~src/interface' +import type { Logger, LoggerInput } from '@exercism/static-analysis' +import { setProcessLogger } from '@exercism/static-analysis' const CONTENTS = `My Fine Output` +/* eslint-disable @typescript-eslint/no-invalid-void-type */ const TEST_LOGGER: Logger & { log: jest.MockInstance } = { error: jest.fn(), log: jest.fn(), fatal: jest.fn(), } +/* eslint-enable @typescript-eslint/no-invalid-void-type */ const DEFAULT_OPTIONS: ExecutionOptions = { debug: false, diff --git a/test/ref.d.ts b/test/ref.d.ts deleted file mode 100644 index 372c8948..00000000 --- a/test/ref.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -interface MockedFs { - __setMockFiles(files: { [path: string]: string }): void - __getWrittenFiles(): { [dir: string]: { [file: string]: string } } -} diff --git a/test/smoke.test.ts b/test/smoke.test.ts index f1e1e8fe..e30d749e 100644 --- a/test/smoke.test.ts +++ b/test/smoke.test.ts @@ -19,7 +19,7 @@ describe('When running analysis', () => { const input = new InlineInput([solutionContent]) const output = await run(analyzer, input, options) - expect(output.comments.length).toBe(0) + expect(output.comments).toHaveLength(0) }) it('can approve with comment', async () => { @@ -57,6 +57,7 @@ describe('When running analysis', () => { describe('When autoloading analyzers', () => { it('can find an analyzer based on an exercise', () => { + // eslint-disable-next-line @typescript-eslint/naming-convention const ActualAnalyzer = find(exercise) expect(ActualAnalyzer).toBe(TwoFerAnalyzer) }) diff --git a/test/tsconfig.eslint.json b/test/tsconfig.eslint.json deleted file mode 100644 index ec159245..00000000 --- a/test/tsconfig.eslint.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "compilerOptions": { "strict": true }, - "include": ["../src/**/*.ts", "./**/*.ts"] -} diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 00000000..02d07d1d --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,16 @@ +{ + "extends": "@tsconfig/recommended/tsconfig.json", + "compilerOptions": { + "target": "ES2016" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "CommonJs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, + "isolatedModules": true /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */, + "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, + "paths": { + "~src/*": ["src/*"], + "~test/*": ["test/*"] + }, + "noEmit": true + }, + "include": ["src", "test"], + "exclude": ["node_modules", "test/fixtures"] +} diff --git a/yarn.lock b/yarn.lock index f8015e51..6c94ae51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -91,6 +91,22 @@ semver "^6.3.0" source-map "^0.5.0" +"@babel/eslint-parser@^7.13.14": + version "7.13.14" + resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.13.14.tgz#f80fd23bdd839537221914cb5d17720a5ea6ba3a" + integrity sha512-I0HweR36D73Ibn/FfrRDMKlMqJHFwidIUgYdMpH+aXYuQC+waq59YaJ6t9e9N36axJ82v1jR041wwqDrDXEwRA== + dependencies: + eslint-scope "^5.1.0" + eslint-visitor-keys "^1.3.0" + semver "^6.3.0" + +"@babel/eslint-plugin@^7.13.15": + version "7.13.15" + resolved "https://registry.yarnpkg.com/@babel/eslint-plugin/-/eslint-plugin-7.13.15.tgz#31eced09690b23de6d7287b8da6dfec09dce9942" + integrity sha512-ZrfhoThsrkcws78a7IJdfUBU5mXIfLiXxjxfIUSWtqYh/F9k1ZCiuibfNJuD5mDsyG3tqPrfE83Qlj/Gfzyi7w== + dependencies: + eslint-rule-composer "^0.3.0" + "@babel/generator@^7.13.0", "@babel/generator@^7.13.9": version "7.13.9" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39" @@ -1279,10 +1295,25 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@exercism/static-analysis@^0.8.1": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@exercism/static-analysis/-/static-analysis-0.8.1.tgz#74596ca63000f979a01e1f2ff4baaf14198c1088" - integrity sha512-0xzPeaII/VkNamHcZ5ixPcqQ63FuZlOu2kiXe1p0zCTrjCW8nwhSeuguKVu7wy7Q4gjvfabd1n8xUOzAbA4PdQ== +"@exercism/eslint-config-javascript@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@exercism/eslint-config-javascript/-/eslint-config-javascript-0.3.1.tgz#8fbc807dcdc86913138e69106c40678171f2a7fe" + integrity sha512-TvFHHNRVPaT1FkXwiULDf8QL7y5Q5FmbbmiICUWA6O2MEK8ZarUEjHWr8aX7MekOj2IhMOIM1PawMBD1RcssVA== + dependencies: + "@babel/eslint-parser" "^7.13.14" + "@babel/eslint-plugin" "^7.13.15" + eslint-config-prettier "^8.2.0" + eslint-plugin-import "^2.22.1" + +"@exercism/eslint-config-tooling@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@exercism/eslint-config-tooling/-/eslint-config-tooling-0.1.0.tgz#a70b3dd8fec09b839f849e91ae8663be7e9fd4b9" + integrity sha512-rGMQ1WVvrR7RiJ66Zw3t2X3QfiNALDwd2G5ky0kdPz83t45I5wZy1potnxGONUEon81l+wy+7QfN8UbTTeCUzQ== + +"@exercism/static-analysis@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@exercism/static-analysis/-/static-analysis-0.9.0.tgz#6f441e29d60fe2962bae504e7875a45f06f0ac78" + integrity sha512-sOaLbKI6yM7lxBV8569OQkiUWXqqrXJuEkuylTPAx8M88bWCy/3q3E2hjW0CPjS5FFKddq7bUMq3Pw3UMWbSDw== "@istanbuljs/load-nyc-config@^1.0.0": version "1.0.0" @@ -1583,6 +1614,19 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/eslint@^7.2.10": + version "7.2.10" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.10.tgz#4b7a9368d46c0f8cd5408c23288a59aa2394d917" + integrity sha512-kUEPnMKrqbtpCq/KTaGFFKAcz6Ethm2EjCoKIDaCmfRBWLbFuTcOJfTlorwbnboXBzahqWLgUp1BQeKHiJzPUQ== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*": + version "0.0.47" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.47.tgz#d7a51db20f0650efec24cd04994f523d93172ed4" + integrity sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg== + "@types/graceful-fs@^4.1.2": version "4.1.4" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.4.tgz#4ff9f641a7c6d1a3508ff88bc3141b152772e753" @@ -1617,6 +1661,11 @@ jest-diff "^26.0.0" pretty-format "^26.0.0" +"@types/json-schema@*": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" + integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== + "@types/json-schema@^7.0.3": version "7.0.3" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" @@ -1632,10 +1681,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.19.tgz#5135176a8330b88ece4e9ab1fdcfc0a545b4bab4" integrity sha512-4nhBPStMK04rruRVtVc6cDqhu7S9GZai0fpXgPXrFpcPX6Xul8xnrjSdGB4KPBVYG/R5+fXWdCM8qBoiULWGPQ== -"@types/node@^14.14.37": - version "14.14.37" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e" - integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw== +"@types/node@^14.14.41": + version "14.14.41" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.41.tgz#d0b939d94c1d7bd53d04824af45f1139b8c45615" + integrity sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -1671,13 +1720,13 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^4.21.0": - version "4.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.21.0.tgz#3fce2bfa76d95c00ac4f33dff369cb593aab8878" - integrity sha512-FPUyCPKZbVGexmbCFI3EQHzCZdy2/5f+jv6k2EDljGdXSRc0cKvbndd2nHZkSLqCNOPk0jB6lGzwIkglXcYVsQ== +"@typescript-eslint/eslint-plugin@^4.22.0": + version "4.22.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.22.0.tgz#3d5f29bb59e61a9dba1513d491b059e536e16dbc" + integrity sha512-U8SP9VOs275iDXaL08Ln1Fa/wLXfj5aTr/1c0t0j6CdbOnxh+TruXu1p4I0NAvdPBQgoPjHsgKn28mOi0FzfoA== dependencies: - "@typescript-eslint/experimental-utils" "4.21.0" - "@typescript-eslint/scope-manager" "4.21.0" + "@typescript-eslint/experimental-utils" "4.22.0" + "@typescript-eslint/scope-manager" "4.22.0" debug "^4.1.1" functional-red-black-tree "^1.0.1" lodash "^4.17.15" @@ -1685,15 +1734,15 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@4.21.0": - version "4.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.21.0.tgz#0b0bb7c15d379140a660c003bdbafa71ae9134b6" - integrity sha512-cEbgosW/tUFvKmkg3cU7LBoZhvUs+ZPVM9alb25XvR0dal4qHL3SiUqHNrzoWSxaXA9gsifrYrS1xdDV6w/gIA== +"@typescript-eslint/experimental-utils@4.22.0": + version "4.22.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.22.0.tgz#68765167cca531178e7b650a53456e6e0bef3b1f" + integrity sha512-xJXHHl6TuAxB5AWiVrGhvbGL8/hbiCQ8FiWwObO3r0fnvBdrbWEDy1hlvGQOAWc6qsCWuWMKdVWlLAEMpxnddg== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/scope-manager" "4.21.0" - "@typescript-eslint/types" "4.21.0" - "@typescript-eslint/typescript-estree" "4.21.0" + "@typescript-eslint/scope-manager" "4.22.0" + "@typescript-eslint/types" "4.22.0" + "@typescript-eslint/typescript-estree" "4.22.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" @@ -1709,14 +1758,14 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@^4.21.0": - version "4.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.21.0.tgz#a227fc2af4001668c3e3f7415d4feee5093894c1" - integrity sha512-eyNf7QmE5O/l1smaQgN0Lj2M/1jOuNg2NrBm1dqqQN0sVngTLyw8tdCbih96ixlhbF1oINoN8fDCyEH9SjLeIA== +"@typescript-eslint/parser@^4.22.0": + version "4.22.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.22.0.tgz#e1637327fcf796c641fe55f73530e90b16ac8fe8" + integrity sha512-z/bGdBJJZJN76nvAY9DkJANYgK3nlRstRRi74WHm3jjgf2I8AglrSY+6l7ogxOmn55YJ6oKZCLLy+6PW70z15Q== dependencies: - "@typescript-eslint/scope-manager" "4.21.0" - "@typescript-eslint/types" "4.21.0" - "@typescript-eslint/typescript-estree" "4.21.0" + "@typescript-eslint/scope-manager" "4.22.0" + "@typescript-eslint/types" "4.22.0" + "@typescript-eslint/typescript-estree" "4.22.0" debug "^4.1.1" "@typescript-eslint/scope-manager@4.12.0": @@ -1727,23 +1776,23 @@ "@typescript-eslint/types" "4.12.0" "@typescript-eslint/visitor-keys" "4.12.0" -"@typescript-eslint/scope-manager@4.21.0": - version "4.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.21.0.tgz#c81b661c4b8af1ec0c010d847a8f9ab76ab95b4d" - integrity sha512-kfOjF0w1Ix7+a5T1knOw00f7uAP9Gx44+OEsNQi0PvvTPLYeXJlsCJ4tYnDj5PQEYfpcgOH5yBlw7K+UEI9Agw== +"@typescript-eslint/scope-manager@4.22.0": + version "4.22.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.22.0.tgz#ed411545e61161a8d702e703a4b7d96ec065b09a" + integrity sha512-OcCO7LTdk6ukawUM40wo61WdeoA7NM/zaoq1/2cs13M7GyiF+T4rxuA4xM+6LeHWjWbss7hkGXjFDRcKD4O04Q== dependencies: - "@typescript-eslint/types" "4.21.0" - "@typescript-eslint/visitor-keys" "4.21.0" + "@typescript-eslint/types" "4.22.0" + "@typescript-eslint/visitor-keys" "4.22.0" "@typescript-eslint/types@4.12.0": version "4.12.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.12.0.tgz#fb891fe7ccc9ea8b2bbd2780e36da45d0dc055e5" integrity sha512-N2RhGeheVLGtyy+CxRmxdsniB7sMSCfsnbh8K/+RUIXYYq3Ub5+sukRCjVE80QerrUBvuEvs4fDhz5AW/pcL6g== -"@typescript-eslint/types@4.21.0": - version "4.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.21.0.tgz#abdc3463bda5d31156984fa5bc316789c960edef" - integrity sha512-+OQaupjGVVc8iXbt6M1oZMwyKQNehAfLYJJ3SdvnofK2qcjfor9pEM62rVjBknhowTkh+2HF+/KdRAc/wGBN2w== +"@typescript-eslint/types@4.22.0": + version "4.22.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.22.0.tgz#0ca6fde5b68daf6dba133f30959cc0688c8dd0b6" + integrity sha512-sW/BiXmmyMqDPO2kpOhSy2Py5w6KvRRsKZnV0c4+0nr4GIcedJwXAq+RHNK4lLVEZAJYFltnnk1tJSlbeS9lYA== "@typescript-eslint/typescript-estree@4.12.0": version "4.12.0" @@ -1759,13 +1808,13 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@4.21.0", "@typescript-eslint/typescript-estree@^4.21.0": - version "4.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.21.0.tgz#3817bd91857beeaeff90f69f1f112ea58d350b0a" - integrity sha512-ZD3M7yLaVGVYLw4nkkoGKumb7Rog7QID9YOWobFDMQKNl+vPxqVIW/uDk+MDeGc+OHcoG2nJ2HphwiPNajKw3w== +"@typescript-eslint/typescript-estree@4.22.0", "@typescript-eslint/typescript-estree@^4.22.0": + version "4.22.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.22.0.tgz#b5d95d6d366ff3b72f5168c75775a3e46250d05c" + integrity sha512-TkIFeu5JEeSs5ze/4NID+PIcVjgoU3cUQUIZnH3Sb1cEn1lBo7StSV5bwPuJQuoxKXlzAObjYTilOEKRuhR5yg== dependencies: - "@typescript-eslint/types" "4.21.0" - "@typescript-eslint/visitor-keys" "4.21.0" + "@typescript-eslint/types" "4.22.0" + "@typescript-eslint/visitor-keys" "4.22.0" debug "^4.1.1" globby "^11.0.1" is-glob "^4.0.1" @@ -1780,12 +1829,12 @@ "@typescript-eslint/types" "4.12.0" eslint-visitor-keys "^2.0.0" -"@typescript-eslint/visitor-keys@4.21.0", "@typescript-eslint/visitor-keys@^4.21.0": - version "4.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.21.0.tgz#990a9acdc124331f5863c2cf21c88ba65233cd8d" - integrity sha512-dH22dROWGi5Z6p+Igc8bLVLmwy7vEe8r+8c+raPQU0LxgogPUrRAtRGtvBWmlr9waTu3n+QLt/qrS/hWzk1x5w== +"@typescript-eslint/visitor-keys@4.22.0", "@typescript-eslint/visitor-keys@^4.22.0": + version "4.22.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.22.0.tgz#169dae26d3c122935da7528c839f42a8a42f6e47" + integrity sha512-nnMu4F+s4o0sll6cBSsTeVsT4cwxB7zECK3dFxzEjPBii9xLpq4yqqsy/FU5zMfan6G60DKZSCXAa3sHJZrcYw== dependencies: - "@typescript-eslint/types" "4.21.0" + "@typescript-eslint/types" "4.22.0" eslint-visitor-keys "^2.0.0" abab@^2.0.3: @@ -2748,10 +2797,10 @@ escodegen@^1.14.1: optionalDependencies: source-map "~0.6.1" -eslint-config-prettier@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.1.0.tgz#4ef1eaf97afe5176e6a75ddfb57c335121abc5a6" - integrity sha512-oKMhGv3ihGbCIimCAjqkdzx2Q+jthoqnXSP+d86M9tptwugycmTFdVR4IpLgq2c4SHifbwO90z2fQ8/Aio73yw== +eslint-config-prettier@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.2.0.tgz#78de77d63bca8e9e59dae75a614b5299925bb7b3" + integrity sha512-dWV9EVeSo2qodOPi1iBYU/x6F6diHv8uujxbxr77xExs3zTAlNXvVZKiyLsQGNz7yPV2K49JY5WjPzNIuDc2Bw== eslint-import-resolver-node@^0.3.4: version "0.3.4" @@ -2788,13 +2837,18 @@ eslint-plugin-import@^2.22.1: resolve "^1.17.0" tsconfig-paths "^3.9.0" -eslint-plugin-jest@^24.3.4: - version "24.3.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-24.3.4.tgz#6d90c3554de0302e879603dd6405474c98849f19" - integrity sha512-3n5oY1+fictanuFkTWPwSlehugBTAgwLnYLFsCllzE3Pl1BwywHl5fL0HFxmMjoQY8xhUDk8uAWc3S4JOHGh3A== +eslint-plugin-jest@^24.3.5: + version "24.3.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-24.3.5.tgz#71f0b580f87915695c286c3f0eb88cf23664d044" + integrity sha512-XG4rtxYDuJykuqhsOqokYIR84/C8pRihRtEpVskYLbIIKGwPNW2ySxdctuVzETZE+MbF/e7wmsnbNVpzM0rDug== dependencies: "@typescript-eslint/experimental-utils" "^4.0.1" +eslint-rule-composer@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" + integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== + eslint-scope@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" @@ -2803,7 +2857,7 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-scope@^5.1.1: +eslint-scope@^5.1.0, eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==