@compodoc/compodoc
Version:
The missing documentation tool for your Angular application
656 lines (611 loc) • 29.3 kB
text/typescript
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);
}
});
}
}