diff --git a/README.md b/README.md index 4ce12add4..95941d598 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,35 @@ dxscanner [path] dxs [path] ``` +## Configuration ⚙️ +Add ```dxscannerrc.*``` config file to change default configuration. It can be a ```.json```, ```.yml``` even dotfile! + +**Practices** +You can switch off practices you don't want to scan or change its impact. Use the id of the practice. + +Possible impact: +``` +high + +medium + +small + +hint + +off +``` + +Example : +``` +{ + "practices": { + "JavaScript.GitignoreCorrectlySet": "medium", + "JavaScript.LoggerUsed": "off" + } +} +``` + ## Contributing 👩‍💻 👨‍💻 Feel free to contribute to the DX Scanner. If you want to contribute, please follow our [Contribution Guide](CONTRIBUTING.md). diff --git a/package.json b/package.json index e8d9f9b1c..16e09a479 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,14 @@ "@oclif/config": "^1", "@oclif/plugin-help": "^2", "@octokit/rest": "^16.28.7", + "@types/js-yaml": "^3.12.1", "axios": "^0.19.0", "colors": "^1.3.3", "debug": "^4.1.1", "git-url-parse": "^11.1.2", "glob": "^7.1.4", "inversify": "^5.0.1", + "js-yaml": "^3.13.1", "lodash": "^4.17.15", "memfs": "^2.15.5", "node-filter-async": "^1.1.3", diff --git a/src/contexts/ConfigProvider.ts b/src/contexts/ConfigProvider.ts new file mode 100644 index 000000000..88385097c --- /dev/null +++ b/src/contexts/ConfigProvider.ts @@ -0,0 +1,44 @@ +import { inject, injectable } from 'inversify'; +import yaml from 'js-yaml'; +import _ from 'lodash'; +import { IFileInspector } from '../inspectors/IFileInspector'; +import { Types } from '../types'; +import { IConfigProvider, Config } from './IConfigProvider'; + +@injectable() +export class ConfigProvider implements IConfigProvider { + private readonly fileInspector: IFileInspector; + config: Config | undefined; + + constructor(@inject(Types.IFileInspector) fileInspector: IFileInspector) { + this.fileInspector = fileInspector; + this.config = undefined; + } + + async init() { + const regexConfigFile = new RegExp('dxscannerrc.', 'i'); + + const configFileMetadata = await this.fileInspector.scanFor(regexConfigFile, '/', { shallow: true }); + + if (configFileMetadata.length === 0) { + return undefined; + } + const configFile = configFileMetadata[0]; + + let parsedContent; + const content = await this.fileInspector.readFile(configFile.path); + + if (configFile.extension === '.json' || configFile.extension === '') { + parsedContent = JSON.parse(content); + } + if (configFile.extension === '.yml' || configFile.extension === '.yaml') { + parsedContent = yaml.safeLoad(content); + } + + this.config = parsedContent; + } + + getOverridenPractice(practiceId: string) { + return _.get(this.config, ['practices', practiceId]); + } +} diff --git a/src/contexts/IConfigProvider.ts b/src/contexts/IConfigProvider.ts new file mode 100644 index 000000000..a2266802f --- /dev/null +++ b/src/contexts/IConfigProvider.ts @@ -0,0 +1,43 @@ +import { PracticeImpact } from '../model'; + +export interface IConfigProvider { + init(): Promise; + getOverridenPractice(practiceId: string): PracticeImpact; +} + +export interface Config { + practices?: { + [key in Practices]?: PracticeImpact; + }; + tokens?: { + [key in Service]: string; + }; +} + +enum Service { + Slack = 'Slack', +} + +enum Practices { + 'JavaScript.TypeScriptUsedPractice' = 'JavaScript.TypeScriptUsedPractice', + 'JavaScript.PrettierUsedPractice' = 'JavaScript.PrettierUsedPractice', + 'JavaScript.ESLintUsedPractice' = 'JavaScript.ESLintUsedPractice', + 'LanguageIndependent.LockfileIsPresentPractice' = 'LanguageIndependent.LockfileIsPresentPractice', + 'JavaScript.JsFrontendTestingFrameworkUsedPractice' = 'JavaScript.JsFrontendTestingFrameworkUsedPractice', + 'JavaScript.JsBackendTestingFrameworkUsedPractice' = 'JavaScript.JsBackendTestingFrameworkUsedPractice', + 'JavaScript.JsLoggerUsedPractice' = 'JavaScript.JsLoggerUsedPractice', + 'LanguageIndependent.LicenseIsPresentPractice' = 'LanguageIndependent.LicenseIsPresentPractice', + 'LanguageIndependent.ReadmeIsPresentPractice' = 'LanguageIndependent.ReadmeIsPresentPractice', + 'LanguageIndependent.CIUsedPractice' = 'LanguageIndependent.CIUsedPractice', + 'JavaScript.JsFEBuildtoolUsedPractice' = 'JavaScript.JsFEBuildtoolUsedPractice', + 'JavaScript.JsPackageJsonConfigurationSetCorrectlyPractice' = 'JavaScript.JsPackageJsonConfigurationSetCorrectlyPractice', + 'JavaScript.JsPackageManagementUsedPractice' = 'JavaScript.JsPackageManagementUsedPractice', + 'JavaScript.DeprecatedTSLintPractice' = 'JavaScript.DeprecatedTSLintPractice', + 'LanguageIndependent.DockerizationUsedPractice' = 'LanguageIndependent.DockerizationUsedPractice', + 'LanguageIndependent.EditorConfigIsPresentPractice' = 'LanguageIndependent.EditorConfigIsPresentPractice', + 'LanguageIndependent.DependenciesVersionPractice' = 'LanguageIndependent.DependenciesVersionPractice', + 'JavaScript.JsGitignoreIsPresentPractice' = 'JavaScript.JsGitignoreIsPresentPractice', + 'JavaScript.JsGitignoreCorrectlySetPractice' = 'JavaScript.JsGitignoreCorrectlySetPractice', + UnitTestPractice = 'UnitTestPractice', + PullRequestPractice = 'PullRequestPractice', +} diff --git a/src/contexts/language/languageContextBinding.ts b/src/contexts/language/languageContextBinding.ts index aa31d6fff..7f0a84531 100644 --- a/src/contexts/language/languageContextBinding.ts +++ b/src/contexts/language/languageContextBinding.ts @@ -1,11 +1,11 @@ import { Container, tagged } from 'inversify'; -import { LanguageContextFactory, Types } from '../../types'; +import { JavaScriptComponentDetector } from '../../detectors/JavaScript/JavaScriptComponentDetector'; +import { FileInspector } from '../../inspectors/FileInspector'; +import { JavaScriptPackageInspector } from '../../inspectors/package/JavaScriptPackageInspector'; import { LanguageAtPath, ProgrammingLanguage } from '../../model'; -import { LanguageContext } from './LanguageContext'; +import { LanguageContextFactory, Types } from '../../types'; import { bindProjectComponentContext } from '../projectComponent/projectComponentContextBinding'; -import { JavaScriptPackageInspector } from '../../inspectors/package/JavaScriptPackageInspector'; -import { FileInspector } from '../../inspectors/FileInspector'; -import { JavaScriptComponentDetector } from '../../detectors/JavaScript/JavaScriptComponentDetector'; +import { LanguageContext } from './LanguageContext'; export const bindLanguageContext = (container: Container) => { container.bind(Types.LanguageContextFactory).toFactory( @@ -21,10 +21,12 @@ export const bindLanguageContext = (container: Container) => { const createLanguageContainer = (languageAtPath: LanguageAtPath, rootContainer: Container): Container => { const container = rootContainer.createChild(); container.bind(Types.LanguageAtPath).toConstantValue(languageAtPath); + bindFileAccess(languageAtPath, container); bindComponentDetectors(container); bindProjectComponentContext(container); bindPackageInspectors(languageAtPath, container); + container.bind(LanguageContext).toSelf(); return container; }; @@ -44,6 +46,8 @@ const bindPackageInspectors = (languageAtPath: LanguageAtPath, container: Contai .bind(Types.IPackageInspector) .to(JavaScriptPackageInspector) .inSingletonScope(); + + // TODO: bind this as InitiableInspector instead of using next line binding container.bind(JavaScriptPackageInspector).toDynamicValue((ctx) => { return ctx.container.get(Types.IPackageInspector); }); diff --git a/src/contexts/projectComponent/ProjectComponentContext.ts b/src/contexts/projectComponent/ProjectComponentContext.ts index f0975329f..68d4301cc 100644 --- a/src/contexts/projectComponent/ProjectComponentContext.ts +++ b/src/contexts/projectComponent/ProjectComponentContext.ts @@ -2,24 +2,29 @@ import { injectable, inject } from 'inversify'; import { Types, PracticeContextFactory } from '../../types'; import { ProgrammingLanguage, ProjectComponent } from '../../model'; import { PracticeContext } from '../practice/PracticeContext'; +import { ConfigProvider } from '../ConfigProvider'; @injectable() export class ProjectComponentContext { readonly projectComponent: ProjectComponent; - get path(): string { - return this.projectComponent.path; - } - get language(): ProgrammingLanguage { - return this.projectComponent.language; - } private readonly practiceContextFactory: PracticeContextFactory; + readonly configProvider: ConfigProvider; constructor( + @inject(Types.ConfigProvider) configProvider: ConfigProvider, @inject(Types.ProjectComponent) projectComponent: ProjectComponent, @inject(Types.PracticeContextFactory) practiceContextFactory: PracticeContextFactory, ) { this.projectComponent = projectComponent; this.practiceContextFactory = practiceContextFactory; + this.configProvider = configProvider; + } + + get path(): string { + return this.projectComponent.path; + } + get language(): ProgrammingLanguage { + return this.projectComponent.language; } getPracticeContext(): PracticeContext { diff --git a/src/contexts/projectComponent/projectComponentContextBinding.ts b/src/contexts/projectComponent/projectComponentContextBinding.ts index 93228cd43..a8d83c3f2 100644 --- a/src/contexts/projectComponent/projectComponentContextBinding.ts +++ b/src/contexts/projectComponent/projectComponentContextBinding.ts @@ -1,9 +1,10 @@ import { Container } from 'inversify'; -import { Types, ProjectComponentContextFactory, PracticeContextFactory } from '../../types'; -import { ProjectComponentContext } from './ProjectComponentContext'; +import { IGitInspector } from '../../inspectors/IGitInspector'; import { ProjectComponent } from '../../model'; +import { PracticeContextFactory, ProjectComponentContextFactory, Types } from '../../types'; +import { ConfigProvider } from '../ConfigProvider'; import { PracticeContext } from '../practice/PracticeContext'; -import { IGitInspector } from '../../inspectors/IGitInspector'; +import { ProjectComponentContext } from './ProjectComponentContext'; export const bindProjectComponentContext = (container: Container) => { container.bind(Types.ProjectComponentContextFactory).toFactory( @@ -18,11 +19,13 @@ export const bindProjectComponentContext = (container: Container) => { const createProjectComponentContainer = (projectComponent: ProjectComponent, rootContainer: Container): Container => { const container = rootContainer.createChild(); + container.bind(Types.ConfigProvider).to(ConfigProvider); container.bind(Types.ProjectComponent).toConstantValue(projectComponent); container.bind(Types.PracticeContextFactory).toFactory( (ctx): PracticeContextFactory => { return (projectComponent: ProjectComponent): PracticeContext => { let gitInspector: IGitInspector | undefined; + try { gitInspector = ctx.container.get(Types.IGitInspector); } catch {} @@ -38,6 +41,7 @@ const createProjectComponentContainer = (projectComponent: ProjectComponent, roo }; }, ); + container.bind(ProjectComponentContext).toSelf(); return container; }; diff --git a/src/contexts/scanner/scannerContextBinding.ts b/src/contexts/scanner/scannerContextBinding.ts index 8d70984cc..c841153a9 100644 --- a/src/contexts/scanner/scannerContextBinding.ts +++ b/src/contexts/scanner/scannerContextBinding.ts @@ -1,13 +1,13 @@ -import { Types, ScannerContextFactory } from '../../types'; -import { ScanningStrategy, ServiceType } from '../../detectors/ScanningStrategyDetector'; -import { ScannerContext } from './ScannerContext'; import { Container } from 'inversify'; -import { bindLanguageContext } from '../language/languageContextBinding'; import { JavaScriptLanguageDetector } from '../../detectors/JavaScript/JavaScriptLanguageDetector'; +import { ScanningStrategy, ServiceType } from '../../detectors/ScanningStrategyDetector'; import { FileInspector } from '../../inspectors/FileInspector'; -import { FileSystemService } from '../../services/FileSystemService'; import { GitInspector } from '../../inspectors/GitInspector'; +import { FileSystemService } from '../../services/FileSystemService'; import { GitHubService } from '../../services/git/GitHubService'; +import { ScannerContextFactory, Types } from '../../types'; +import { bindLanguageContext } from '../language/languageContextBinding'; +import { ScannerContext } from './ScannerContext'; export const bindScanningContext = (container: Container) => { container.bind(Types.ScannerContextFactory).toFactory( @@ -42,7 +42,6 @@ const bindFileAccess = (scanningStrategy: ScanningStrategy, container: Container if (scanningStrategy.serviceType === ServiceType.github) { container.bind(Types.IContentRepositoryBrowser).to(GitHubService); } - // TODO: bind services for GitHub strategy container .bind(Types.IFileInspector) .to(FileInspector) diff --git a/src/index.ts b/src/index.ts index a213a7332..8417c1ca6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import { createRootContainer } from './inversify.config'; import { Scanner } from './scanner/Scanner'; import { Command, flags } from '@oclif/command'; import cli from 'cli-ux'; +import { ServiceError } from './lib/errors'; class DXScannerCommand extends Command { static description = 'Scan your project for possible DX recommendations.'; @@ -40,12 +41,16 @@ class DXScannerCommand extends Command { try { await scanner.scan(); } catch (error) { - authorization = await cli.prompt('Insert your GitHub personal access token.\nhttps://github.com/settings/tokens\n'); + if (error instanceof ServiceError) { + authorization = await cli.prompt('Insert your GitHub personal access token.\nhttps://github.com/settings/tokens\n'); - const container = createRootContainer({ uri: scanPath, auth: authorization }); - const scanner = container.get(Scanner); + const container = createRootContainer({ uri: scanPath, auth: authorization, json: json }); + const scanner = container.get(Scanner); - await scanner.scan(); + await scanner.scan(); + } else { + throw error; + } } cli.action.stop(); diff --git a/src/inversify.config.ts b/src/inversify.config.ts index 1059c9789..4306588cc 100644 --- a/src/inversify.config.ts +++ b/src/inversify.config.ts @@ -1,26 +1,26 @@ import { Container } from 'inversify'; -import { Scanner } from './scanner/Scanner'; -import { Types } from './types'; -import { IReporter } from './reporters/IReporter'; -import { CLIReporter } from './reporters/CLIReporter'; -import { practices } from './practices'; -import { ScanningStrategyDetector } from './detectors/ScanningStrategyDetector'; -import { bindScanningContext } from './contexts/scanner/scannerContextBinding'; -import { FileSystemService } from './services/FileSystemService'; -import { GitHubService } from './services/git/GitHubService'; +import { DirectoryJSON } from 'memfs/lib/volume'; import { PracticeContext } from './contexts/practice/PracticeContext'; -import { IPackageInspector } from './inspectors/IPackageInspector'; -import { IFileInspector } from './inspectors/IFileInspector'; -import { ProgrammingLanguage, ProjectComponentType, ProjectComponentPlatform, ProjectComponentFramework, ProjectComponent } from './model'; -import { JavaScriptPackageInspector } from './inspectors/package/JavaScriptPackageInspector'; +import { bindScanningContext } from './contexts/scanner/scannerContextBinding'; +import { ScanningStrategyDetector } from './detectors/ScanningStrategyDetector'; import { packageJSONContents } from './detectors/__MOCKS__'; -import { IPracticeWithMetadata } from './practices/DxPracticeDecorator'; -import { ScannerUtils } from './scanner/ScannerUtils'; +import { CollaborationInspector } from './inspectors/CollaborationInspector'; import { FileInspector } from './inspectors/FileInspector'; +import { IFileInspector } from './inspectors/IFileInspector'; +import { IPackageInspector } from './inspectors/IPackageInspector'; import { IssueTrackingInspector } from './inspectors/IssueTrackingInspector'; -import { CollaborationInspector } from './inspectors/CollaborationInspector'; -import { DirectoryJSON } from 'memfs/lib/volume'; +import { JavaScriptPackageInspector } from './inspectors/package/JavaScriptPackageInspector'; +import { ProgrammingLanguage, ProjectComponent, ProjectComponentFramework, ProjectComponentPlatform, ProjectComponentType } from './model'; +import { practices } from './practices'; +import { IPracticeWithMetadata } from './practices/DxPracticeDecorator'; +import { CLIReporter } from './reporters/CLIReporter'; +import { IReporter } from './reporters/IReporter'; import { JSONReporter } from './reporters/JSONReporter'; +import { Scanner } from './scanner/Scanner'; +import { ScannerUtils } from './scanner/ScannerUtils'; +import { FileSystemService } from './services/FileSystemService'; +import { GitHubService } from './services/git/GitHubService'; +import { Types } from './types'; export const createRootContainer = (args: ArgumentsProvider): Container => { const container = new Container(); diff --git a/src/model.ts b/src/model.ts index 4e45c41c8..53be2afbb 100644 --- a/src/model.ts +++ b/src/model.ts @@ -90,6 +90,7 @@ export interface DeprecatedProjectComponent { } export interface PracticeMetadata { + defaultImpact?: PracticeImpact; id: string; name: string; suggestion: string; @@ -104,6 +105,7 @@ export enum PracticeImpact { medium = 'medium', small = 'small', hint = 'hint', + off = 'off', } export enum PracticeEvaluationResult { diff --git a/src/practices/DxPracticeDecorator.ts b/src/practices/DxPracticeDecorator.ts index 6dc19b98e..0efe745c2 100644 --- a/src/practices/DxPracticeDecorator.ts +++ b/src/practices/DxPracticeDecorator.ts @@ -9,7 +9,7 @@ function DxPracticeWrapperDecorator(practiceMetadata: PracticeMetadata) { return function classDecorator {}>(constructor: T) { return class extends constructor { getMetadata = () => { - return { ...practiceMetadata, matcher: this }; + return { ...practiceMetadata, defaultImpact: practiceMetadata.impact, matcher: this }; }; }; }; diff --git a/src/reporters/CLIReporter.ts b/src/reporters/CLIReporter.ts index 81b3056f0..7e14653e7 100644 --- a/src/reporters/CLIReporter.ts +++ b/src/reporters/CLIReporter.ts @@ -1,13 +1,14 @@ -import { Color, blue, bold, green, grey, italic, red, reset, yellow } from 'colors'; +import { Color, blue, bold, green, grey, italic, red, reset, yellow, underline } from 'colors'; import { PracticeAndComponent, PracticeImpact } from '../model'; import { GitHubUrlParser } from '../services/git/GitHubUrlParser'; import { IReporter } from './IReporter'; import { injectable } from 'inversify'; import { uniq, compact } from 'lodash'; +import { IPracticeWithMetadata } from '../practices/DxPracticeDecorator'; @injectable() export class CLIReporter implements IReporter { - report(practicesAndComponents: PracticeAndComponent[]): string { + report(practicesAndComponents: PracticeAndComponent[], practicesOff: IPracticeWithMetadata[]): string { const lines: string[] = []; const repoNames = uniq( @@ -41,6 +42,15 @@ export class CLIReporter implements IReporter { const impactLine = this.emitImpactSegment(practicesAndComponents, impact); impactLine && lines.push(impactLine); } + + lines.push('----------------------------'); + lines.push(''); + practicesOff.length === 0 + ? lines.push(bold(yellow('No practice was switched off.'))) + : lines.push(bold(red('You switched off these practices:'))); + for (const practice of practicesOff) { + lines.push(red(`- ${italic(practice.getMetadata().name)}`)); + } lines.push(''); lines.push('----------------------------'); lines.push(''); @@ -74,6 +84,9 @@ export class CLIReporter implements IReporter { } for (const pac of practicesAndComponents) { lines.push(this.linesForPractice(pac, color, practicesAndComponents.length > 1)); + if (pac.practice.defaultImpact !== pac.practice.impact) { + lines.push(bold(this.changedImpact(pac, (color = grey)))); + } } lines.push(bold('')); return lines.join('\n'); @@ -94,4 +107,17 @@ export class CLIReporter implements IReporter { return practiceLineTexts.join(' '); } + + private changedImpact(pac: PracticeAndComponent, color: Color) { + const practiceLineTexts = [ + reset( + color( + `You changed impact of ${bold(pac.practice.name)} from ${underline(pac.practice.defaultImpact)} to ${underline( + pac.practice.impact, + )}`, + ), + ), + ]; + return practiceLineTexts.join(' '); + } } diff --git a/src/reporters/IReporter.ts b/src/reporters/IReporter.ts index a7bccad6a..00b349b3b 100644 --- a/src/reporters/IReporter.ts +++ b/src/reporters/IReporter.ts @@ -6,12 +6,13 @@ import { ProjectComponent, PracticeMetadata, } from '../model'; +import { IPracticeWithMetadata } from '../practices/DxPracticeDecorator'; export interface IReporter { - report(practicesAndComponents: PracticeAndComponent[]): string | Promise; + report(practicesAndComponents: PracticeAndComponent[], practicesOff: IPracticeWithMetadata[]): string | JSONReport; } -export type JSONReport = { uri: string; components: ComponentReport[] }; +export type JSONReport = { uri: string; components: ComponentReport[]; practicesOff?: string[] }; export interface ComponentReport extends ProjectComponent { path: string; diff --git a/src/reporters/JSONReporter.ts b/src/reporters/JSONReporter.ts index 19813d2ab..dcdc55f44 100644 --- a/src/reporters/JSONReporter.ts +++ b/src/reporters/JSONReporter.ts @@ -4,6 +4,7 @@ import { injectable, inject } from 'inversify'; import { Types } from '../types'; import { ArgumentsProvider } from '../inversify.config'; import _ from 'lodash'; +import { IPracticeWithMetadata } from '../practices/DxPracticeDecorator'; @injectable() export class JSONReporter implements IReporter { @@ -13,7 +14,7 @@ export class JSONReporter implements IReporter { this.argumentsProvider = argumentsProvider; } - async report(practicesAndComponents: PracticeAndComponent[]): Promise { + report(practicesAndComponents: PracticeAndComponent[], practicesOff: IPracticeWithMetadata[]): JSONReport { const report: JSONReport = { uri: this.argumentsProvider.uri, components: [], @@ -28,6 +29,10 @@ export class JSONReporter implements IReporter { continue; } component.practices.push(pac.practice); + + if (practicesOff.length > 0) { + Object.assign(component, { practicesOff: practicesOff }); + } } return report; diff --git a/src/scanner/Scanner.ts b/src/scanner/Scanner.ts index 2fcaaf02c..353d2a738 100644 --- a/src/scanner/Scanner.ts +++ b/src/scanner/Scanner.ts @@ -1,31 +1,29 @@ -import { injectable, inject, multiInject } from 'inversify'; -import { ScanningStrategyDetector, ScanningStrategy, ServiceType } from '../detectors/ScanningStrategyDetector'; -import { Types, ScannerContextFactory } from '../types'; +import debug from 'debug'; +import fs from 'fs'; +import { inject, injectable, multiInject } from 'inversify'; +import os from 'os'; +import path from 'path'; +import git from 'simple-git/promise'; +import url from 'url'; +import util, { inspect } from 'util'; +import { LanguageContext } from '../contexts/language/LanguageContext'; +import { PracticeContext } from '../contexts/practice/PracticeContext'; +import { ProjectComponentContext } from '../contexts/projectComponent/ProjectComponentContext'; +import { ScannerContext } from '../contexts/scanner/ScannerContext'; +import { ScanningStrategy, ScanningStrategyDetector, ServiceType } from '../detectors/ScanningStrategyDetector'; +import { ArgumentsProvider } from '../inversify.config'; import { LanguageAtPath, - ProjectComponent, PracticeEvaluationResult, + ProjectComponent, ProjectComponentFramework, ProjectComponentPlatform, ProjectComponentType, } from '../model'; -import { ScannerContext } from '../contexts/scanner/ScannerContext'; -import { LanguageContext } from '../contexts/language/LanguageContext'; -import { PracticeContext } from '../contexts/practice/PracticeContext'; -import { ProjectComponentContext } from '../contexts/projectComponent/ProjectComponentContext'; +import { IPracticeWithMetadata } from '../practices/DxPracticeDecorator'; import { IReporter } from '../reporters/IReporter'; -import debug from 'debug'; -import fs from 'fs'; -import git from 'simple-git/promise'; -import os from 'os'; -import path from 'path'; -import url from 'url'; -import { inspect } from 'util'; +import { ScannerContextFactory, Types } from '../types'; import { ScannerUtils } from './ScannerUtils'; -import { ArgumentsProvider } from '../inversify.config'; -import { IPracticeWithMetadata } from '../practices/DxPracticeDecorator'; -import filterAsync from 'node-filter-async'; -import util from 'util'; @injectable() export class Scanner { @@ -63,7 +61,7 @@ export class Scanner { const projectComponents = await this.detectProjectComponents(languagesAtPaths, scannerContext, scanStrategy); this.scanDebug(`Components:`, inspect(projectComponents)); const identifiedPractices = await this.detectPractices(projectComponents); - await this.report(identifiedPractices); + await this.report(identifiedPractices.practicesWithContext, identifiedPractices.practicesOff); } private async preprocessData(scanningStrategy: ScanningStrategy) { @@ -134,24 +132,23 @@ export class Scanner { return components; } - private async detectPractices(componentsWithContext: ProjectComponentAndLangContext[]): Promise { + private async detectPractices(componentsWithContext: ProjectComponentAndLangContext[]): Promise { const practicesWithContext: PracticeWithContext[] = []; + let filteredPractices; for (const componentWithCtx of componentsWithContext) { const practicesWithContextFromComponent: PracticeWithContext[] = []; const componentContext = componentWithCtx.languageContext.getProjectComponentContext(componentWithCtx.component); const practiceContext = componentContext.getPracticeContext(); - const applicablePractices = await filterAsync(this.practices, async (p) => { - return await p.isApplicable(practiceContext); - }); + await componentContext.configProvider.init(); + filteredPractices = await ScannerUtils.filterPractices(componentContext, this.practices); + const orderedApplicablePractices = ScannerUtils.sortPractices(filteredPractices.customApplicablePractices); - const orderedApplicablePractices = ScannerUtils.sortPractices(applicablePractices); for (const practice of orderedApplicablePractices) { const isFulfilled = ScannerUtils.isFulfilled(practice, practicesWithContextFromComponent); if (!isFulfilled) continue; - const evaluation = await practice.evaluate(practiceContext); practicesWithContext.push({ practice, @@ -161,22 +158,33 @@ export class Scanner { }); } } + this.scanDebug('Applicable practices:'); this.scanDebug(practicesWithContext.map((p) => p.practice.getMetadata().name)); - return practicesWithContext; + + return { practicesWithContext: practicesWithContext, practicesOff: filteredPractices ? filteredPractices.practicesOff : [] }; } - private async report(practicesWithContext: PracticeWithContext[]) { + private async report(practicesWithContext: PracticeWithContext[], practicesOff: IPracticeWithMetadata[]) { const relevantPractices = practicesWithContext.filter((p) => p.evaluation === PracticeEvaluationResult.notPracticing); + const reportString = this.reporter.report( relevantPractices.map((p) => { + const impact = p.componentContext.configProvider.getOverridenPractice(p.practice.getMetadata().id); + return { - practice: p.practice.getMetadata(), + practice: { + ...p.practice.getMetadata(), + impact: impact ? impact : p.practice.getMetadata().impact, + }, component: p.componentContext.projectComponent, }; }), + practicesOff, ); - console.log(reportString); + typeof reportString === 'string' + ? console.log(reportString) + : console.log(util.inspect(reportString, { showHidden: false, depth: null })); } } @@ -191,3 +199,8 @@ export interface PracticeWithContext { practice: IPracticeWithMetadata; evaluation: PracticeEvaluationResult; } + +interface PracticeWithContextAndOff { + practicesWithContext: PracticeWithContext[]; + practicesOff: IPracticeWithMetadata[]; +} diff --git a/src/scanner/ScannerUtils.spec.ts b/src/scanner/ScannerUtils.spec.ts index 1d849bcc0..8d80d931f 100644 --- a/src/scanner/ScannerUtils.spec.ts +++ b/src/scanner/ScannerUtils.spec.ts @@ -1,10 +1,20 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { ScannerUtils } from './ScannerUtils'; +import _ from 'lodash'; +import { PracticeContext } from '../contexts/practice/PracticeContext'; +import { + PracticeEvaluationResult, + PracticeImpact, + ProgrammingLanguage, + ProjectComponentFramework, + ProjectComponentPlatform, + ProjectComponentType, +} from '../model'; import { DeprecatedTSLintPractice } from '../practices/JavaScript/DeprecatedTSLintPractice'; import { ESLintUsedPractice } from '../practices/JavaScript/ESLintUsedPractice'; +import { JsGitignoreCorrectlySetPractice } from '../practices/JavaScript/JsGitignoreCorrectlySetPractice'; import { TypeScriptUsedPractice } from '../practices/JavaScript/TypeScriptUsedPractice'; -import { FirstTestPractice, SecondTestPractice, InvalidTestPractice } from './__MOCKS__'; -import { PracticeEvaluationResult } from '../model'; +import { ScannerUtils } from './ScannerUtils'; +import { FirstTestPractice, InvalidTestPractice, SecondTestPractice } from './__MOCKS__'; describe('ScannerUtils', () => { describe('#sortPractices', () => { @@ -59,5 +69,44 @@ describe('ScannerUtils', () => { expect(result).toEqual(false); }); + + it('filterPractices() returns filtered out practices and practices off', async () => { + const config = { + practices: { + 'JavaScript.GitignoreCorrectlySet': PracticeImpact.off, + }, + }; + + const componentMock = { + framework: ProjectComponentFramework.UNKNOWN, + language: ProgrammingLanguage.JavaScript, + path: './var/foo', + platform: ProjectComponentPlatform.BackEnd, + type: ProjectComponentType.Application, + }; + + const componentContext = { + configProvider: { + getOverridenPractice(practiceId: string) { + return _.get(config, ['practices', practiceId]); + }, + }, + + getPracticeContext(): PracticeContext { + return { + projectComponent: componentMock, + } as any; + }, + }; + + const practices = [DeprecatedTSLintPractice, ESLintUsedPractice, TypeScriptUsedPractice, JsGitignoreCorrectlySetPractice].map( + ScannerUtils.initPracticeWithMetadata, + ); + + const filteredPractices = await ScannerUtils.filterPractices(componentContext as any, practices); + + expect(filteredPractices.practicesOff.length).toBeGreaterThanOrEqual(1); + expect(filteredPractices.customApplicablePractices.length).toBeGreaterThanOrEqual(2); + }); }); }); diff --git a/src/scanner/ScannerUtils.ts b/src/scanner/ScannerUtils.ts index 781d4ef36..cdf061f7e 100644 --- a/src/scanner/ScannerUtils.ts +++ b/src/scanner/ScannerUtils.ts @@ -1,9 +1,12 @@ -import toposort from 'toposort'; import _ from 'lodash'; -import { IPractice } from '../practices/IPractice'; +import filterAsync from 'node-filter-async'; +import toposort from 'toposort'; +import { ProjectComponentContext } from '../contexts/projectComponent/ProjectComponentContext'; +import { ErrorFactory } from '../lib/errors'; +import { PracticeImpact } from '../model'; import { IPracticeWithMetadata } from '../practices/DxPracticeDecorator'; +import { IPractice } from '../practices/IPractice'; import { PracticeWithContext } from './Scanner'; -import { ErrorFactory } from '../lib/errors'; /** * Scanner helpers & utilities @@ -74,4 +77,27 @@ export class ScannerUtils { return true; } + + /** + * Filter out applicable practices and turned off practices. + */ + static async filterPractices(componentContext: ProjectComponentContext, practices: IPracticeWithMetadata[]) { + const practiceContext = componentContext.getPracticeContext(); + + //need practiceContext.projectComponent + const applicablePractices = await filterAsync(practices, async (p) => { + return await p.isApplicable(practiceContext); + }); + + /* Filter out turned off practices */ + const customApplicablePractices = applicablePractices.filter( + (p) => componentContext.configProvider.getOverridenPractice(p.getMetadata().id) !== PracticeImpact.off, + ); + + const practicesOff = applicablePractices.filter( + (p) => componentContext.configProvider.getOverridenPractice(p.getMetadata().id) === PracticeImpact.off, + ); + + return { customApplicablePractices, practicesOff }; + } } diff --git a/src/types.ts b/src/types.ts index db5ab52e5..d45482859 100644 --- a/src/types.ts +++ b/src/types.ts @@ -43,6 +43,7 @@ export const Types = { ArgumentsProvider: Symbol('ArgumentsProvider'), Practice: Symbol('Practice'), IReporter: Symbol('IReporter'), + ConfigProvider: Symbol('ConfigProvider'), }; export type GitFactory = (repository: Repository) => Git; diff --git a/yarn.lock b/yarn.lock index e4d283185..459bbf1dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -708,6 +708,11 @@ dependencies: "@types/jest-diff" "*" +"@types/js-yaml@^3.12.1": + version "3.12.1" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.1.tgz#5c6f4a1eabca84792fbd916f0cb40847f123c656" + integrity sha512-SGGAhXLHDx+PK4YLNcNGa6goPf9XRWQNAUUbffkwVGGXIxmDKWyGGL4inzq2sPmExu431Ekb9aEMn9BkPqEYFA== + "@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"