jscrs
Version:
comment-rate-statistics
220 lines (204 loc) • 7.29 kB
text/typescript
import { prepareOptions, filterFilesByExtensionNameInOptions } from './lib/options'
import { getRegExpsByExtensionName, RegExps } from './lib/RegExps'
import { getExtensionName } from './lib/fsHelper'
import * as path from 'path'
import * as fs from 'fs'
import { IOptions } from './interfaces/options.i'
import { IAnalysis } from './interfaces/analysis.i'
import { IReport } from './interfaces/report.i'
import { sync } from 'fast-glob'
import { isNullOrUndefined } from './lib/utils'
import { registerReporterByName, getRegisteredReporters } from './reporter'
import { relative } from 'path'
import { HTMLLIKE_EXTS, CSSLIKE_EXTS } from './lib/formats'
/**
* 代码注释率检查入口
*/
export default class analysis implements IAnalysis {
private _conf: IOptions
get conf(): IOptions {
return this._conf
}
set conf(value: IOptions) {
this._conf = value
}
private _fileList: string[]
get fileList(): string[] {
return this._fileList
}
set fileList(value: string[]) {
this._fileList = value
}
private _ext2FileList: Map<string, string[]>
get ext2FileList(): Map<string, string[]> {
return this._ext2FileList
}
set ext2FileList(value: Map<string, string[]>) {
this._ext2FileList = value
}
private _rateReport: Map<string, IReport>
get rateReport(): Map<string, IReport> {
return this._rateReport
}
set rateReport(value: Map<string, IReport>) {
this._rateReport = value
}
constructor(options?: IOptions) {
this._conf = isNullOrUndefined(options) ? prepareOptions() : options as IOptions
this._fileList = this.getFiles()
this._ext2FileList = this.getExt2FileList()
this._rateReport = new Map()
this.initializeReporters()
// console.log(this.ext2FileList)
}
private getExt2FileList(): Map<string, string[]> {
let ext2FileList: Map<string, string[]> = new Map()
this.conf.ext.forEach(x => {
ext2FileList.set(
x,
this.fileList.filter(y => path.extname(y).substr(1) === x)
)
})
return ext2FileList
}
private getFiles(): string[] {
const { path, ignore, ext } = this._conf
let fileList = sync(path, {
ignore,
onlyFiles: true,
dot: false,
stats: false,
absolute: true,
})
return filterFilesByExtensionNameInOptions(fileList, ext)
}
/**
* 统计代码注释率
*/
public async statisticCommentRate() {
try {
let i = 0
while (i < this.fileList.length) {
let filePath = this._fileList[i]
let rateReport = await this.statisticSingleFileCommentLength(filePath)
this._rateReport.set(filePath, rateReport)
i++
}
this._rateReport.set('sum', this.statisticSumRate())
return this.rateReport
} catch (error) {
throw error
}
}
/**
* 统计单个文件的注释长度
* @param regs 文件扩展名所对应的正则表达式列表
* @param filePath 文件路径
*/
private statisticSingleFileCommentLength(filePath: string): Promise<IReport> {
return new Promise((resolve, reject) => {
fs.readFile(filePath, { encoding: 'utf-8' }, (err, fdata) => {
if (err) {
console.error(err)
return reject(err)
}
let fileLength: number = fdata.length
let singleFileCommentLength: number = 0
const extname: string = getExtensionName(filePath)
// html-likes 文件单独处理
if (HTMLLIKE_EXTS.includes(extname)) {
let tmpObj = this.statisticHtmlLikeStream(fdata, extname)
fileLength = tmpObj.fileLength
singleFileCommentLength = tmpObj.commentLength
} else if (extname === 'md') {
// md文档算作注释
singleFileCommentLength = fileLength
} else {
const regs: RegExp[] = getRegExpsByExtensionName(extname)
singleFileCommentLength = this.calculateCommentLength(regs, fdata)
}
const commentRate = ((singleFileCommentLength / (fileLength === 0 ? 1 : fileLength)) * 100).toFixed(1)
let logstr: IReport = {
extname: extname,
filePath: relative(process.cwd(), filePath),
length: fileLength,
commentLength: singleFileCommentLength,
commentRate: `${commentRate}%`,
}
// console.table([
// logstr
// ])
resolve(logstr)
})
})
}
/**
* 如果ext配置项内存在 *css,则不过滤
*/
private kickOutStyleStreamIfNoCSSLikeExtensionNames(fileExtName: string, fileData: string): string {
if (HTMLLIKE_EXTS.includes(fileExtName) && !this.checkIfCssLikeExtInOptions()) {
return fileData.replace(RegExps.style, '')
}
return fileData
}
/**
* 检查配置项内有无css相关检查要求
*/
private checkIfCssLikeExtInOptions() {
return CSSLIKE_EXTS.filter(x => this._conf.ext.includes(x)).length > 0
}
private calculateCommentLength(regs: RegExp[], fileStream: string) {
return regs.reduce((pre, cur) => {
const m = fileStream.match(cur) || []
return pre + this.calculateCommentLengthByMatchArr(m)
}, 0)
}
/**
* 统计单个正则匹配的注释字符串长度
* @param match 正则匹配字符串得到的结果
*/
private calculateCommentLengthByMatchArr(match: RegExpMatchArray): number {
return match.reduce((p, c) => p + c.length, 0)
}
private statisticHtmlLikeStream(fileStream: string, extname: string) {
const str = this.kickOutStyleStreamIfNoCSSLikeExtensionNames(extname, fileStream)
let commentLength = 0
const fileLength = str.length
// 单独统计template模板内的标签文本注释
commentLength += this.calculateCommentLength([RegExps.markup], str)
// 单独解析script标签内的js代码注释
const scriptMatch = str.match(RegExps.script) || []
scriptMatch.forEach(x => {
commentLength += this.calculateCommentLength([RegExps.singleLine, RegExps.multiLine], x)
})
// 单独统计style标签内的样式代码注释
if (this.checkIfCssLikeExtInOptions()) {
;(str.match(RegExps.style) || []).forEach(x => {
commentLength += this.calculateCommentLength([RegExps.singleLine, RegExps.multiLine], x)
})
}
return { fileLength, commentLength }
}
private statisticSumRate() {
let report = [...this._rateReport.values()]
let sumLength = report.reduce((pre, cur) => pre + cur.length, 0)
let sumCommentLength = report.reduce((pre, cur) => pre + cur.commentLength, 0)
let sumReport: IReport = {
extname: 'SUM',
length: sumLength,
commentLength: sumCommentLength,
commentRate: `${((sumCommentLength / sumLength) * 100).toFixed(1)}%`,
}
return sumReport
}
public async generateReports() {
const rateReport = await this.statisticCommentRate()
Object.values(getRegisteredReporters()).map(reporter => {
reporter.report(rateReport)
})
return rateReport
}
private initializeReporters() {
registerReporterByName(this._conf)
}
}