diff --git a/.eslintrc.js b/.eslintrc.js index 2dc335df..a4cbeb7e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,9 @@ +const { rules: baseBestPracticesRules } = require('eslint-config-airbnb-base/rules/best-practices'); +const { rules: baseErrorsRules } = require('eslint-config-airbnb-base/rules/errors'); +const { rules: baseES6Rules } = require('eslint-config-airbnb-base/rules/es6'); +const { rules: baseImportsRules } = require('eslint-config-airbnb-base/rules/imports'); const { rules: baseStyleRules } = require('eslint-config-airbnb-base/rules/style'); +const { rules: baseVariablesRules } = require('eslint-config-airbnb-base/rules/variables'); module.exports = { root: true, @@ -39,6 +44,18 @@ module.exports = { mocha: true, }, }, + { + files: ['**/*.ts?(x)', '**/*.d.ts'], + parserOptions: { + // Setting project is required for some rules such as @typescript-eslint/dot-notation, + // @typescript-eslint/return-await and @typescript-eslint/no-throw-literal. + // If this property is missing they fail due to missing parser. + project: ['./tsconfig.json'], + }, + rules: { + ...getTypeScriptOverrides(), + }, + }, { files: ['**/tests/**/*.{j,t}s?(x)'], rules: { @@ -113,3 +130,161 @@ function getOpinionatedRuleOverrides() { ], }; } + +function getTypeScriptOverrides() { + /* + Here until Vue supports AirBnb Typescript overrides (vuejs/eslint-config-airbnb#23). + Based on `eslint-config-airbnb-typescript`. + Source: https://github.com/iamturns/eslint-config-airbnb-typescript/blob/v16.1.0/lib/shared.js + It cannot be used directly due to compilation errors. + */ + return { + 'brace-style': 'off', + '@typescript-eslint/brace-style': baseStyleRules['brace-style'], + + camelcase: 'off', + '@typescript-eslint/naming-convention': [ + 'error', + { selector: 'variable', format: ['camelCase', 'PascalCase', 'UPPER_CASE'] }, + { selector: 'function', format: ['camelCase', 'PascalCase'] }, + { selector: 'typeLike', format: ['PascalCase'] }, + ], + + 'comma-dangle': 'off', + '@typescript-eslint/comma-dangle': [ + baseStyleRules['comma-dangle'][0], + { + ...baseStyleRules['comma-dangle'][1], + enums: baseStyleRules['comma-dangle'][1].arrays, + generics: baseStyleRules['comma-dangle'][1].arrays, + tuples: baseStyleRules['comma-dangle'][1].arrays, + }, + ], + + 'comma-spacing': 'off', + '@typescript-eslint/comma-spacing': baseStyleRules['comma-spacing'], + + 'default-param-last': 'off', + '@typescript-eslint/default-param-last': baseBestPracticesRules['default-param-last'], + + 'dot-notation': 'off', + '@typescript-eslint/dot-notation': baseBestPracticesRules['dot-notation'], + + 'func-call-spacing': 'off', + '@typescript-eslint/func-call-spacing': baseStyleRules['func-call-spacing'], + + // ❌ Broken for some cases, but still useful. + // Here until Prettifier is used. + indent: 'off', + '@typescript-eslint/indent': baseStyleRules.indent, + + 'keyword-spacing': 'off', + '@typescript-eslint/keyword-spacing': baseStyleRules['keyword-spacing'], + + 'lines-between-class-members': 'off', + '@typescript-eslint/lines-between-class-members': baseStyleRules['lines-between-class-members'], + + 'no-array-constructor': 'off', + '@typescript-eslint/no-array-constructor': baseStyleRules['no-array-constructor'], + + 'no-dupe-class-members': 'off', + '@typescript-eslint/no-dupe-class-members': baseES6Rules['no-dupe-class-members'], + + 'no-empty-function': 'off', + '@typescript-eslint/no-empty-function': baseBestPracticesRules['no-empty-function'], + + 'no-extra-parens': 'off', + '@typescript-eslint/no-extra-parens': baseErrorsRules['no-extra-parens'], + + 'no-extra-semi': 'off', + '@typescript-eslint/no-extra-semi': baseErrorsRules['no-extra-semi'], + + // ❌ Fails due to missing parser + // 'no-implied-eval': 'off', + // 'no-new-func': 'off', + // '@typescript-eslint/no-implied-eval': baseBestPracticesRules['no-implied-eval'], + + 'no-loss-of-precision': 'off', + '@typescript-eslint/no-loss-of-precision': baseErrorsRules['no-loss-of-precision'], + + 'no-loop-func': 'off', + '@typescript-eslint/no-loop-func': baseBestPracticesRules['no-loop-func'], + + 'no-magic-numbers': 'off', + '@typescript-eslint/no-magic-numbers': baseBestPracticesRules['no-magic-numbers'], + + 'no-redeclare': 'off', + '@typescript-eslint/no-redeclare': baseBestPracticesRules['no-redeclare'], + + // ESLint variant does not work with TypeScript enums. + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': baseVariablesRules['no-shadow'], + + 'no-throw-literal': 'off', + '@typescript-eslint/no-throw-literal': baseBestPracticesRules['no-throw-literal'], + + 'no-unused-expressions': 'off', + '@typescript-eslint/no-unused-expressions': baseBestPracticesRules['no-unused-expressions'], + + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': baseVariablesRules['no-unused-vars'], + + // https://erkinekici.com/articles/linting-trap#no-use-before-define + // 'no-use-before-define': 'off', + // '@typescript-eslint/no-use-before-define': baseVariablesRules['no-use-before-define'], + + // ESLint variant does not understand TypeScript constructors. + // eslint/eslint/#14118, typescript-eslint/typescript-eslint#873 + 'no-useless-constructor': 'off', + '@typescript-eslint/no-useless-constructor': baseES6Rules['no-useless-constructor'], + + quotes: 'off', + '@typescript-eslint/quotes': baseStyleRules.quotes, + + semi: 'off', + '@typescript-eslint/semi': baseStyleRules.semi, + + 'space-before-function-paren': 'off', + '@typescript-eslint/space-before-function-paren': baseStyleRules['space-before-function-paren'], + + 'require-await': 'off', + '@typescript-eslint/require-await': baseBestPracticesRules['require-await'], + + 'no-return-await': 'off', + '@typescript-eslint/return-await': baseBestPracticesRules['no-return-await'], + + 'space-infix-ops': 'off', + '@typescript-eslint/space-infix-ops': baseStyleRules['space-infix-ops'], + + 'object-curly-spacing': 'off', + '@typescript-eslint/object-curly-spacing': baseStyleRules['object-curly-spacing'], + + 'import/extensions': [ + baseImportsRules['import/extensions'][0], + baseImportsRules['import/extensions'][1], + { + ...baseImportsRules['import/extensions'][2], + ts: 'never', + tsx: 'never', + }, + ], + + // Changes required is not yet implemented: + // 'import/no-extraneous-dependencies': [ + // baseImportsRules['import/no-extraneous-dependencies'][0], + // { + // ...baseImportsRules['import/no-extraneous-dependencies'][1], + // devDependencies: baseImportsRules[ + // 'import/no-extraneous-dependencies' + // ][1].devDependencies.reduce((result, devDep) => { + // const toAppend = [devDep]; + // const devDepWithTs = devDep.replace(/\bjs(x?)\b/g, 'ts$1'); + // if (devDepWithTs !== devDep) { + // toAppend.push(devDepWithTs); + // } + // return [...result, ...toAppend]; + // }, []), + // }, + // ], + }; +} diff --git a/src/application/ApplicationFactory.ts b/src/application/ApplicationFactory.ts index edfc0657..0ab31304 100644 --- a/src/application/ApplicationFactory.ts +++ b/src/application/ApplicationFactory.ts @@ -3,15 +3,15 @@ import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy'; import { IApplicationFactory } from './IApplicationFactory'; import { parseApplication } from './Parser/ApplicationParser'; -export type ApplicationGetter = () => IApplication; -const ApplicationGetter: ApplicationGetter = parseApplication; +export type ApplicationGetterType = () => IApplication; +const ApplicationGetter: ApplicationGetterType = parseApplication; export class ApplicationFactory implements IApplicationFactory { public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter); private readonly getter: AsyncLazy; - protected constructor(costlyGetter: ApplicationGetter) { + protected constructor(costlyGetter: ApplicationGetterType) { if (!costlyGetter) { throw new Error('undefined getter'); } diff --git a/src/application/Context/IApplicationContext.ts b/src/application/Context/IApplicationContext.ts index 59bf5892..bd1b2c32 100644 --- a/src/application/Context/IApplicationContext.ts +++ b/src/application/Context/IApplicationContext.ts @@ -10,8 +10,8 @@ export interface IReadOnlyApplicationContext { } export interface IApplicationContext extends IReadOnlyApplicationContext { - readonly state: ICategoryCollectionState; - changeContext(os: OperatingSystem): void; + readonly state: ICategoryCollectionState; + changeContext(os: OperatingSystem): void; } export interface IApplicationContextChangedEvent { diff --git a/src/application/Parser/Script/Compiler/Expressions/Expression/Expression.ts b/src/application/Parser/Script/Compiler/Expressions/Expression/Expression.ts index fdc2566a..9142bace 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Expression/Expression.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Expression/Expression.ts @@ -12,7 +12,7 @@ export class Expression implements IExpression { public readonly position: ExpressionPosition, public readonly evaluator: ExpressionEvaluator, public readonly parameters - : IReadOnlyFunctionParameterCollection = new FunctionParameterCollection(), + : IReadOnlyFunctionParameterCollection = new FunctionParameterCollection(), ) { if (!position) { throw new Error('undefined position'); diff --git a/src/presentation/electron/main.ts b/src/presentation/electron/main.ts index 813c31b6..58698ffa 100644 --- a/src/presentation/electron/main.ts +++ b/src/presentation/electron/main.ts @@ -14,7 +14,7 @@ import { setupAutoUpdater } from './Update/Updater'; const isDevelopment = process.env.NODE_ENV !== 'production'; // Path of static assets, magic variable populated by electron -// eslint-disable-next-line no-underscore-dangle +// eslint-disable-next-line @typescript-eslint/naming-convention, no-underscore-dangle declare const __static: string; // https://github.com/electron-userland/electron-webpack/issues/172 // Keep a global reference of the window object, if you don't, the window will diff --git a/tests/unit/application/ApplicationFactory.spec.ts b/tests/unit/application/ApplicationFactory.spec.ts index 40a34746..751c542e 100644 --- a/tests/unit/application/ApplicationFactory.spec.ts +++ b/tests/unit/application/ApplicationFactory.spec.ts @@ -1,6 +1,6 @@ import 'mocha'; import { expect } from 'chai'; -import { ApplicationFactory, ApplicationGetter } from '@/application/ApplicationFactory'; +import { ApplicationFactory, ApplicationGetterType } from '@/application/ApplicationFactory'; import { ApplicationStub } from '@tests/unit/stubs/ApplicationStub'; describe('ApplicationFactory', () => { @@ -19,7 +19,7 @@ describe('ApplicationFactory', () => { it('returns result from the getter', async () => { // arrange const expected = new ApplicationStub(); - const getter: ApplicationGetter = () => expected; + const getter: ApplicationGetterType = () => expected; const sut = new SystemUnderTest(getter); // act const actual = await Promise.all([ @@ -35,7 +35,7 @@ describe('ApplicationFactory', () => { // arrange let totalExecution = 0; const expected = new ApplicationStub(); - const getter: ApplicationGetter = () => { + const getter: ApplicationGetterType = () => { totalExecution++; return expected; }; @@ -54,7 +54,7 @@ describe('ApplicationFactory', () => { }); class SystemUnderTest extends ApplicationFactory { - public constructor(costlyGetter: ApplicationGetter) { + public constructor(costlyGetter: ApplicationGetterType) { super(costlyGetter); } } diff --git a/tests/unit/application/Parser/ApplicationParser.spec.ts b/tests/unit/application/Parser/ApplicationParser.spec.ts index cf3cb5a2..bd40ac36 100644 --- a/tests/unit/application/Parser/ApplicationParser.spec.ts +++ b/tests/unit/application/Parser/ApplicationParser.spec.ts @@ -138,8 +138,8 @@ describe('ApplicationParser', () => { class CategoryCollectionParserSpy { public arguments = new Array<{ - data: CollectionData, - info: ProjectInformation, + data: CollectionData, + info: ProjectInformation, }>(); private returnValues = new Map(); diff --git a/tests/unit/infrastructure/Threading/AsyncSleep.spec.ts b/tests/unit/infrastructure/Threading/AsyncSleep.spec.ts index 09e9d931..978a8b91 100644 --- a/tests/unit/infrastructure/Threading/AsyncSleep.spec.ts +++ b/tests/unit/infrastructure/Threading/AsyncSleep.spec.ts @@ -42,7 +42,7 @@ class SchedulerMock { private currentTime = 0; - private scheduledActions = new Array<{time: number, action: SchedulerCallbackType}>(); + private scheduledActions = new Array<{ time: number, action: SchedulerCallbackType }>(); constructor() { this.mock = (callback: SchedulerCallbackType, ms: number) => { diff --git a/tests/unit/stubs/CodeSubstituterStub.ts b/tests/unit/stubs/CodeSubstituterStub.ts index bf7da7c3..90d43315 100644 --- a/tests/unit/stubs/CodeSubstituterStub.ts +++ b/tests/unit/stubs/CodeSubstituterStub.ts @@ -3,7 +3,7 @@ import { ICodeSubstituter } from '@/application/Parser/ScriptingDefinition/ICode export class CodeSubstituterStub implements ICodeSubstituter { private readonly scenarios = - new Array<{ code: string, info: IProjectInformation, result: string}>(); + new Array<{ code: string, info: IProjectInformation, result: string }>(); public substitute(code: string, info: IProjectInformation): string { const scenario = this.scenarios.find((s) => s.code === code && s.info === info); diff --git a/tests/unit/stubs/EnumParserStub.ts b/tests/unit/stubs/EnumParserStub.ts index f77deb2a..bdf7705b 100644 --- a/tests/unit/stubs/EnumParserStub.ts +++ b/tests/unit/stubs/EnumParserStub.ts @@ -2,7 +2,7 @@ import { IEnumParser } from '@/application/Common/Enum'; export class EnumParserStub implements IEnumParser { private readonly scenarios = - new Array<{ inputName: string, inputValue: string, outputValue: T }>(); + new Array<{ inputName: string, inputValue: string, outputValue: T }>(); private defaultValue: T; diff --git a/tests/unit/stubs/ExpressionsCompilerStub.ts b/tests/unit/stubs/ExpressionsCompilerStub.ts index a91fb7f4..80c3d1a9 100644 --- a/tests/unit/stubs/ExpressionsCompilerStub.ts +++ b/tests/unit/stubs/ExpressionsCompilerStub.ts @@ -6,7 +6,7 @@ import { FunctionCallArgumentCollectionStub } from '@tests/unit/stubs/FunctionCa export class ExpressionsCompilerStub implements IExpressionsCompiler { public readonly callHistory = - new Array<{ code: string, parameters: IReadOnlyFunctionCallArgumentCollection }>(); + new Array<{ code: string, parameters: IReadOnlyFunctionCallArgumentCollection }>(); private readonly scenarios = new Array(); diff --git a/tests/unit/stubs/UserSelectionStub.ts b/tests/unit/stubs/UserSelectionStub.ts index 10f67adb..b0019a9b 100644 --- a/tests/unit/stubs/UserSelectionStub.ts +++ b/tests/unit/stubs/UserSelectionStub.ts @@ -6,7 +6,7 @@ import { EventSource } from '@/infrastructure/Events/EventSource'; export class UserSelectionStub implements IUserSelection { public readonly changed: IEventSource = - new EventSource(); + new EventSource(); public selectedScripts: readonly SelectedScript[] = [];