UNPKG

@compodoc/compodoc

Version:

The missing documentation tool for your Angular application

656 lines (611 loc) 29.3 kB
import * as _ from '../../utils/collection.util'; import { SyntaxKind } from 'ts-morph'; import { logger } from '../../utils/logger'; import Configuration from '../configuration'; import HtmlEngine from '../engines/html.engine'; import { COMPODOC_DEFAULTS } from '../../utils/defaults'; type GenerationPromiseResolve = (value: boolean | PromiseLike<boolean>) => void; type GenerationPromiseReject = (reason?: any) => void; export class DocumentationCoverageUseCase { constructor( private readonly generationPromiseResolve: GenerationPromiseResolve, private readonly generationPromiseReject: GenerationPromiseReject ) {} public execute(): Promise<boolean> { logger.info('Process documentation coverage report'); return new Promise((resolve, reject) => { /* * loop with components, directives, controllers, entities, classes, injectables, interfaces, pipes, guards, misc functions variables */ let files: any[] = []; let totalProjectStatementDocumented = 0; const coverageExcludePatterns = Configuration.mainData.coverageExclude || []; const toUnixPath = (filePath: string): string => (filePath || '').replace(/\\/g, '/'); const escapeRegex = (value: string): string => value.replace(/[|\\{}()[\]^$+?.]/g, '\\$&'); const globToRegExp = (globPattern: string): RegExp => { const normalized = toUnixPath(globPattern); const escaped = escapeRegex(normalized) .replace(/\*\*/g, '::DOUBLE_STAR::') .replace(/\*/g, '[^/]*') .replace(/::DOUBLE_STAR::/g, '.*'); return new RegExp(`^${escaped}$`); }; const coverageExcludeRegexes = coverageExcludePatterns .map((pattern: any) => String(pattern || '').trim()) .filter((pattern: string) => pattern.length > 0) .map((pattern: string) => globToRegExp(pattern)); const hasCoverageIgnoredTag = (tags: any): boolean => { if (!tags || !Array.isArray(tags)) { return false; } return tags.some((tag: any) => { if (!tag) { return false; } const explicitTagName = tag.tagName?.text || tag.tagName?.escapedText || tag.tagName || tag.name?.text || tag.name || ''; if (explicitTagName) { return String(explicitTagName).toLowerCase() === 'coverageignore'; } if (typeof tag === 'string') { return tag.toLowerCase().indexOf('coverageignore') > -1; } if (typeof tag.label === 'string') { return tag.label.toLowerCase().indexOf('coverageignore') > -1; } return false; }); }; const isCoverageIgnoredElement = (element: any): boolean => { if (!element) { return false; } if (element.coverageIgnore === true) { return true; } if (hasCoverageIgnoredTag(element.jsdoctags)) { return true; } return false; }; const isCoverageIgnoredByFile = (filePath: string): boolean => { if (!coverageExcludeRegexes.length || !filePath) { return false; } const normalizedPath = toUnixPath(filePath); return coverageExcludeRegexes.some((regex: RegExp) => regex.test(normalizedPath)); }; const shouldSkipCoverage = (element: any): boolean => { if (!element) { return true; } return ( isCoverageIgnoredByFile(element.file || element.filePath) || isCoverageIgnoredElement(element) ); }; const getStatus = function (percent: number) { let status; if (percent <= 25) { status = 'low'; } else if (percent > 25 && percent <= 50) { status = 'medium'; } else if (percent > 50 && percent <= 75) { status = 'good'; } else { status = 'very-good'; } return status; }; const processComponentsAndDirectivesAndControllersAndEntities = (list: any) => { _.forEach(list, (el: any) => { const element = (Object as any).assign({}, el); if (shouldSkipCoverage(element)) { return; } if (!element.propertiesClass) { element.propertiesClass = []; } if (!element.methodsClass) { element.methodsClass = []; } if (!element.hostBindings) { element.hostBindings = []; } if (!element.hostListeners) { element.hostListeners = []; } if (!element.inputsClass) { element.inputsClass = []; } if (!element.outputsClass) { element.outputsClass = []; } const cl: any = { filePath: element.file, type: element.type, linktype: element.type, name: element.name }; let totalStatementDocumented = 0; let totalStatements = element.propertiesClass.length + element.methodsClass.length + element.inputsClass.length + element.hostBindings.length + element.hostListeners.length + element.outputsClass.length + 1; // +1 for element decorator comment if (element.constructorObj) { totalStatements += 1; if ( element.constructorObj && element.constructorObj.description && element.constructorObj.description !== '' ) { totalStatementDocumented += 1; } } if (element.description && element.description !== '') { totalStatementDocumented += 1; } _.forEach(element.propertiesClass, (property: any) => { if (isCoverageIgnoredElement(property)) { totalStatements -= 1; return; } if (property.modifierKind === SyntaxKind.PrivateKeyword) { // Doesn't handle private for coverage totalStatements -= 1; } if ( property.description && property.description !== '' && property.modifierKind !== SyntaxKind.PrivateKeyword ) { totalStatementDocumented += 1; } }); _.forEach(element.methodsClass, (method: any) => { if (isCoverageIgnoredElement(method)) { totalStatements -= 1; return; } if (method.modifierKind === SyntaxKind.PrivateKeyword) { // Doesn't handle private for coverage totalStatements -= 1; } if ( method.description && method.description !== '' && method.modifierKind !== SyntaxKind.PrivateKeyword ) { totalStatementDocumented += 1; } }); _.forEach(element.hostBindings, (property: any) => { if (isCoverageIgnoredElement(property)) { totalStatements -= 1; return; } if (property.modifierKind === SyntaxKind.PrivateKeyword) { // Doesn't handle private for coverage totalStatements -= 1; } if ( property.description && property.description !== '' && property.modifierKind !== SyntaxKind.PrivateKeyword ) { totalStatementDocumented += 1; } }); _.forEach(element.hostListeners, (method: any) => { if (isCoverageIgnoredElement(method)) { totalStatements -= 1; return; } if (method.modifierKind === SyntaxKind.PrivateKeyword) { // Doesn't handle private for coverage totalStatements -= 1; } if ( method.description && method.description !== '' && method.modifierKind !== SyntaxKind.PrivateKeyword ) { totalStatementDocumented += 1; } }); _.forEach(element.inputsClass, (input: any) => { if (isCoverageIgnoredElement(input)) { totalStatements -= 1; return; } if (input.modifierKind === SyntaxKind.PrivateKeyword) { // Doesn't handle private for coverage totalStatements -= 1; } if ( input.description && input.description !== '' && input.modifierKind !== SyntaxKind.PrivateKeyword ) { totalStatementDocumented += 1; } }); _.forEach(element.outputsClass, (output: any) => { if (isCoverageIgnoredElement(output)) { totalStatements -= 1; return; } if (output.modifierKind === SyntaxKind.PrivateKeyword) { // Doesn't handle private for coverage totalStatements -= 1; } if ( output.description && output.description !== '' && output.modifierKind !== SyntaxKind.PrivateKeyword ) { totalStatementDocumented += 1; } }); cl.coveragePercent = Math.floor( (totalStatementDocumented / totalStatements) * 100 ); if (totalStatements === 0) { cl.coveragePercent = 0; } cl.coverageCount = totalStatementDocumented + '/' + totalStatements; cl.status = getStatus(cl.coveragePercent); totalProjectStatementDocumented += cl.coveragePercent; files.push(cl); }); }; let processCoveragePerFile = () => { logger.info('Process documentation coverage per file'); logger.info('-------------------'); let overFiles = files.filter((f: any) => { let overTest = f.coveragePercent >= Configuration.mainData.coverageMinimumPerFile; if (overTest && !Configuration.mainData.coverageTestShowOnlyFailed) { logger.info( `${f.coveragePercent} % for file ${f.filePath} - ${f.name} - over minimum per file` ); } return overTest; }); let underFiles = files.filter((f: any) => { let underTest = f.coveragePercent < Configuration.mainData.coverageMinimumPerFile; if (underTest) { logger.error( `${f.coveragePercent} % for file ${f.filePath} - ${f.name} - under minimum per file` ); } return underTest; }); logger.info('-------------------'); return { overFiles: overFiles, underFiles: underFiles }; }; let processFunctionsAndVariables = (id: any, type: any) => { _.forEach(id, (el: any) => { if (shouldSkipCoverage(el)) { return; } let cl: any = { filePath: el.file, type: type, linktype: el.type, linksubtype: el.subtype, name: el.name }; if (type === 'variable' || type === 'function' || type === 'type alias') { cl.linktype = 'miscellaneous'; } let totalStatementDocumented = 0; let totalStatements = 1; if (el.modifierKind === SyntaxKind.PrivateKeyword) { // Doesn't handle private for coverage totalStatements -= 1; } if ( el.description && el.description !== '' && el.modifierKind !== SyntaxKind.PrivateKeyword ) { totalStatementDocumented += 1; } cl.coveragePercent = Math.floor( (totalStatementDocumented / totalStatements) * 100 ); cl.coverageCount = totalStatementDocumented + '/' + totalStatements; cl.status = getStatus(cl.coveragePercent); totalProjectStatementDocumented += cl.coveragePercent; files.push(cl); }); }; let processClasses = (list: any, type: any, linktype: any) => { _.forEach(list, (cl: any) => { let element = (Object as any).assign({}, cl); if (shouldSkipCoverage(element)) { return; } if (!element.properties) { element.properties = []; } if (!element.methods) { element.methods = []; } let cla: any = { filePath: element.file, type: type, linktype: linktype, name: element.name }; let totalStatementDocumented = 0; let totalStatements = element.properties.length + element.methods.length + 1; // +1 for element itself if (element.constructorObj) { totalStatements += 1; if ( element.constructorObj && element.constructorObj.description && element.constructorObj.description !== '' ) { totalStatementDocumented += 1; } } if (element.description && element.description !== '') { totalStatementDocumented += 1; } _.forEach(element.properties, (property: any) => { if (isCoverageIgnoredElement(property)) { totalStatements -= 1; return; } if (property.modifierKind === SyntaxKind.PrivateKeyword) { // Doesn't handle private for coverage totalStatements -= 1; } if ( property.description && property.description !== '' && property.modifierKind !== SyntaxKind.PrivateKeyword ) { totalStatementDocumented += 1; } }); _.forEach(element.methods, (method: any) => { if (isCoverageIgnoredElement(method)) { totalStatements -= 1; return; } if (method.modifierKind === SyntaxKind.PrivateKeyword) { // Doesn't handle private for coverage totalStatements -= 1; } if ( method.description && method.description !== '' && method.modifierKind !== SyntaxKind.PrivateKeyword ) { totalStatementDocumented += 1; } }); cla.coveragePercent = Math.floor( (totalStatementDocumented / totalStatements) * 100 ); if (totalStatements === 0) { cla.coveragePercent = 0; } cla.coverageCount = totalStatementDocumented + '/' + totalStatements; cla.status = getStatus(cla.coveragePercent); totalProjectStatementDocumented += cla.coveragePercent; files.push(cla); }); }; processComponentsAndDirectivesAndControllersAndEntities( Configuration.mainData.components ); processComponentsAndDirectivesAndControllersAndEntities( Configuration.mainData.directives ); processComponentsAndDirectivesAndControllersAndEntities( Configuration.mainData.controllers ); processComponentsAndDirectivesAndControllersAndEntities( Configuration.mainData.entities ); processClasses(Configuration.mainData.classes, 'class', 'classe'); processClasses(Configuration.mainData.injectables, 'injectable', 'injectable'); processClasses(Configuration.mainData.interfaces, 'interface', 'interface'); processClasses(Configuration.mainData.guards, 'guard', 'guard'); processClasses(Configuration.mainData.interceptors, 'interceptor', 'interceptor'); _.forEach(Configuration.mainData.pipes, (pipe: any) => { if (shouldSkipCoverage(pipe)) { return; } let cl: any = { filePath: pipe.file, type: pipe.type, linktype: pipe.type, name: pipe.name }; let totalStatementDocumented = 0; let totalStatements = 1; if (pipe.description && pipe.description !== '') { totalStatementDocumented += 1; } cl.coveragePercent = Math.floor((totalStatementDocumented / totalStatements) * 100); cl.coverageCount = totalStatementDocumented + '/' + totalStatements; cl.status = getStatus(cl.coveragePercent); totalProjectStatementDocumented += cl.coveragePercent; files.push(cl); }); processFunctionsAndVariables( Configuration.mainData.miscellaneous.functions, 'function' ); processFunctionsAndVariables( Configuration.mainData.miscellaneous.variables, 'variable' ); processFunctionsAndVariables( Configuration.mainData.miscellaneous.typealiases, 'type alias' ); files = _.sortBy(files, ['filePath']); let coverageData = { count: files.length > 0 ? Math.floor(totalProjectStatementDocumented / files.length) : 0, status: '', files }; coverageData.status = getStatus(coverageData.count); Configuration.addPage({ name: 'coverage', id: 'coverage', context: 'coverage', files: files, data: coverageData, depth: 0, pageType: COMPODOC_DEFAULTS.PAGE_TYPES.ROOT }); coverageData.files = files; Configuration.mainData.coverageData = coverageData; if (Configuration.mainData.exportFormat === COMPODOC_DEFAULTS.exportFormat) { HtmlEngine.generateCoverageBadge( Configuration.mainData.output, 'documentation', coverageData ); } files = _.sortBy(files, ['coveragePercent']); let coverageTestPerFileResults; if ( Configuration.mainData.coverageTest && !Configuration.mainData.coverageTestPerFile ) { // Global coverage test and not per file if (coverageData.count >= Configuration.mainData.coverageTestThreshold) { logger.info( `Documentation coverage (${coverageData.count}%) is over threshold (${Configuration.mainData.coverageTestThreshold}%)` ); this.generationPromiseResolve(true); process.exit(0); } else { let message = `Documentation coverage (${coverageData.count}%) is not over threshold (${Configuration.mainData.coverageTestThreshold}%)`; this.generationPromiseReject(); if (Configuration.mainData.coverageTestThresholdFail) { logger.error(message); process.exit(1); } else { logger.warn(message); process.exit(0); } } } else if ( !Configuration.mainData.coverageTest && Configuration.mainData.coverageTestPerFile ) { coverageTestPerFileResults = processCoveragePerFile(); // Per file coverage test and not global if (coverageTestPerFileResults.underFiles.length > 0) { let message = `Documentation coverage per file is not over threshold (${Configuration.mainData.coverageMinimumPerFile}%)`; this.generationPromiseReject(); if (Configuration.mainData.coverageTestThresholdFail) { logger.error(message); process.exit(1); } else { logger.warn(message); process.exit(0); } } else { logger.info( `Documentation coverage per file is over threshold (${Configuration.mainData.coverageMinimumPerFile}%)` ); this.generationPromiseResolve(true); process.exit(0); } } else if ( Configuration.mainData.coverageTest && Configuration.mainData.coverageTestPerFile ) { // Per file coverage test and global coverageTestPerFileResults = processCoveragePerFile(); if ( coverageData.count >= Configuration.mainData.coverageTestThreshold && coverageTestPerFileResults.underFiles.length === 0 ) { logger.info( `Documentation coverage (${coverageData.count}%) is over threshold (${Configuration.mainData.coverageTestThreshold}%)` ); logger.info( `Documentation coverage per file is over threshold (${Configuration.mainData.coverageMinimumPerFile}%)` ); this.generationPromiseResolve(true); process.exit(0); } else if ( coverageData.count >= Configuration.mainData.coverageTestThreshold && coverageTestPerFileResults.underFiles.length > 0 ) { logger.info( `Documentation coverage (${coverageData.count}%) is over threshold (${Configuration.mainData.coverageTestThreshold}%)` ); let message = `Documentation coverage per file is not over threshold (${Configuration.mainData.coverageMinimumPerFile}%)`; this.generationPromiseReject(); if (Configuration.mainData.coverageTestThresholdFail) { logger.error(message); process.exit(1); } else { logger.warn(message); process.exit(0); } } else if ( coverageData.count < Configuration.mainData.coverageTestThreshold && coverageTestPerFileResults.underFiles.length > 0 ) { let messageGlobal = `Documentation coverage (${coverageData.count}%) is not over threshold (${Configuration.mainData.coverageTestThreshold}%)`, messagePerFile = `Documentation coverage per file is not over threshold (${Configuration.mainData.coverageMinimumPerFile}%)`; this.generationPromiseReject(); if (Configuration.mainData.coverageTestThresholdFail) { logger.error(messageGlobal); logger.error(messagePerFile); process.exit(1); } else { logger.warn(messageGlobal); logger.warn(messagePerFile); process.exit(0); } } else { let message = `Documentation coverage (${coverageData.count}%) is not over threshold (${Configuration.mainData.coverageTestThreshold}%)`, messagePerFile = `Documentation coverage per file is over threshold (${Configuration.mainData.coverageMinimumPerFile}%)`; this.generationPromiseReject(); if (Configuration.mainData.coverageTestThresholdFail) { logger.error(message); logger.info(messagePerFile); process.exit(1); } else { logger.warn(message); logger.info(messagePerFile); process.exit(0); } } } else { resolve(true); } }); } }