Skip to content

Commit

Permalink
Add option to ignore error messages and just track errors by type (#32)
Browse files Browse the repository at this point in the history
* Version baseline file and add helpful associated CLI logic

* Add initial support to ignore messages and only track error count by type
  • Loading branch information
wdoug authored Jun 21, 2024
1 parent 1ba40b0 commit 39c4e6b
Show file tree
Hide file tree
Showing 4 changed files with 392 additions and 66 deletions.
75 changes: 66 additions & 9 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import {
addHashToBaseline,
getNewErrors,
parseTypeScriptErrors,
readTypeScriptErrorsFromFile,
getTotalErrorsCount,
toHumanReadableText,
writeTypeScriptErrorsToFile
writeTypeScriptErrorsToFile,
readBaselineErrorsFile,
isBaselineVersionCurrent,
getErrorSummaryMap,
getBaselineFileVersion,
CURRENT_BASELINE_VERSION
} from './util'
import { resolve } from 'path'
import { rmSync } from 'fs'
Expand All @@ -28,10 +32,16 @@ import { rmSync } from 'fs'
`Path to file to save baseline errors to. Defaults to .tsc-baseline.json`
)

program.option(
'--ignoreMessages',
'Ignores specific type error messages and only counts errors by code.'
)

const getConfig = () => {
const config = program.opts()
return {
path: resolve(process.cwd(), config.path || '.tsc-baseline.json')
path: resolve(process.cwd(), config.path || '.tsc-baseline.json'),
ignoreMessages: config.ignoreMessages || false
}
}

Expand All @@ -40,9 +50,13 @@ import { rmSync } from 'fs'
message = stdin
if (message) {
const config = getConfig()
const errorOptions = {
ignoreMessages: config.ignoreMessages
}
writeTypeScriptErrorsToFile(
parseTypeScriptErrors(message).errorSummaryMap,
config.path
parseTypeScriptErrors(message, errorOptions).errorSummaryMap,
config.path,
errorOptions
)
console.log("\nSaved baseline errors to '" + config.path + "'")
}
Expand All @@ -63,9 +77,52 @@ import { rmSync } from 'fs'
message = stdin
if (message) {
const config = getConfig()
const oldErrorSummaries = readTypeScriptErrorsFromFile(config.path)
const { specificErrorsMap, errorSummaryMap } =
parseTypeScriptErrors(message)
let baselineFile
try {
baselineFile = readBaselineErrorsFile(config.path)
} catch (err) {
console.error(
`
Unable to read the .tsc-baseline.json file at "${config.path}".
Has the baseline file been properly saved with the 'save' command?
`
)
process.exit(1)
}
if (!isBaselineVersionCurrent(baselineFile)) {
const baselineFileVersion = getBaselineFileVersion(baselineFile)
if (baselineFileVersion < CURRENT_BASELINE_VERSION) {
console.error(
`
The .tsc-baseline.json file at "${config.path}"
is out of date for this version of tsc-baseline.
Please update the baseline file using the 'save' command.
`
)
process.exit(1)
} else {
console.error(
`
The .tsc-baseline.json file at "${config.path}"
is from a future version of tsc-baseline.
Are your installed packages up to date?
`
)
process.exit(1)
}
}

const oldErrorSummaries = getErrorSummaryMap(baselineFile)
const errorOptions = {
ignoreMessages: baselineFile.meta.ignoreMessages
}
const { specificErrorsMap, errorSummaryMap } = parseTypeScriptErrors(
message,
errorOptions
)
const newErrorSummaries = getNewErrors(
oldErrorSummaries,
errorSummaryMap
Expand All @@ -78,7 +135,7 @@ import { rmSync } from 'fs'
} found`

console.error(`${newErrorsCount > 0 ? '\nNew errors found:' : ''}
${toHumanReadableText(newErrorSummaries, specificErrorsMap)}
${toHumanReadableText(newErrorSummaries, specificErrorsMap, errorOptions)}
${newErrorsCountMessage}. ${oldErrorsCount} error${
oldErrorsCount === 1 ? '' : 's'
Expand Down
116 changes: 96 additions & 20 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { readFileSync, writeFileSync } from 'fs'
import objectHash from 'object-hash'

export const CURRENT_BASELINE_VERSION = 1

export interface ErrorSummary {
code: string
count: number
file: string
message: string
message?: string
}

export interface SpecificError {
Expand All @@ -16,6 +18,21 @@ export interface SpecificError {
message: string
}

export interface OldBaselineFile {
[hash: string]: ErrorSummary | SpecificError
}

export interface BaselineFile {
meta: {
baselineFileVersion: number
ignoreMessages: boolean
}
// eslint-disable-next-line typescript-sort-keys/interface
errors: {
[hash: string]: ErrorSummary
}
}

export type SpecificErrorsMap = Map<string, SpecificError[]>
export type ErrorSummaryMap = Map<string, ErrorSummary>

Expand All @@ -24,14 +41,27 @@ export interface ParsingResult {
specificErrorsMap: SpecificErrorsMap
}

type ErrorOptions = {
ignoreMessages: boolean
}

// Hash just the error summary, not the count so that we can easily
// modify the count independently
const getErrorSummaryHash = (errorSummary: ErrorSummary) => {
const { count, ...rest } = errorSummary
return objectHash(rest)
const getErrorSummaryHash = (
errorSummary: ErrorSummary,
{ ignoreMessages }: ErrorOptions
) => {
const { code, file, message } = errorSummary
if (ignoreMessages) {
return objectHash({ code, file })
}
return objectHash({ code, file, message })
}

export const parseTypeScriptErrors = (errorLog: string): ParsingResult => {
export const parseTypeScriptErrors = (
errorLog: string,
{ ignoreMessages }: ErrorOptions
): ParsingResult => {
const errorPattern = /^(.+)\((\d+),(\d+)\): error (\w+): (.+)$/

const lines = errorLog.split('\n')
Expand All @@ -58,10 +88,12 @@ export const parseTypeScriptErrors = (errorLog: string): ParsingResult => {
let errorSummary: ErrorSummary = {
file,
code,
message,
count: 1
}
const key = getErrorSummaryHash(errorSummary)
if (!ignoreMessages) {
errorSummary.message = message
}
const key = getErrorSummaryHash(errorSummary, { ignoreMessages })

const existingError = errorSummaryMap.get(key)
if (existingError) {
Expand Down Expand Up @@ -92,20 +124,48 @@ export const parseTypeScriptErrors = (errorLog: string): ParsingResult => {

export const writeTypeScriptErrorsToFile = (
map: ErrorSummaryMap,
filepath: string
filepath: string,
errorOptions: ErrorOptions
): void => {
writeFileSync(filepath, JSON.stringify(Object.fromEntries(map), null, 2))
const newBaselineFile: BaselineFile = {
meta: {
baselineFileVersion: CURRENT_BASELINE_VERSION,
ignoreMessages: errorOptions.ignoreMessages
},
errors: Object.fromEntries(map)
}
writeFileSync(filepath, JSON.stringify(newBaselineFile, null, 2))
}

export const readTypeScriptErrorsFromFile = (
export const readBaselineErrorsFile = (
filepath: string
): ErrorSummaryMap => {
): BaselineFile | OldBaselineFile => {
const text = readFileSync(filepath, {
encoding: 'utf-8'
})
const json = JSON.parse(text)
return JSON.parse(text)
}

export const getBaselineFileVersion = (
baselineFile: BaselineFile | OldBaselineFile
) => {
if (
typeof baselineFile?.meta === 'object' &&
'baselineFileVersion' in baselineFile?.meta
) {
return baselineFile?.meta?.baselineFileVersion
}
return 0
}

export const isBaselineVersionCurrent = (
baselineFile: BaselineFile | OldBaselineFile
): baselineFile is BaselineFile => {
return getBaselineFileVersion(baselineFile) === CURRENT_BASELINE_VERSION
}

return new Map(Object.entries(json))
export const getErrorSummaryMap = (baselineFile: BaselineFile) => {
return new Map(Object.entries(baselineFile.errors))
}

export const getNewErrors = (
Expand Down Expand Up @@ -139,18 +199,22 @@ export const getTotalErrorsCount = (errorMap: ErrorSummaryMap): number =>

export const toHumanReadableText = (
errorSummaryMap: ErrorSummaryMap,
specificErrorMap: SpecificErrorsMap
specificErrorMap: SpecificErrorsMap,
errorOptions: ErrorOptions
): string => {
let log = ''

for (const [key, error] of errorSummaryMap) {
const specificErrors = getSpecificErrorsMatchingSummary(
error,
specificErrorMap
specificErrorMap,
errorOptions
)

log += `File: ${error.file}\n`
log += `Message: ${error.message}\n`
if (error.message) {
log += `Message: ${error.message}\n`
}
log += `Code: ${error.code}\n`
log += `Hash: ${key}\n`
log += `Count of new errors: ${error.count}\n`
Expand All @@ -173,22 +237,32 @@ export const toHumanReadableText = (

export const getSpecificErrorsMatchingSummary = (
errorSummary: ErrorSummary,
specificErrorsMap: SpecificErrorsMap
specificErrorsMap: SpecificErrorsMap,
errorOptions: ErrorOptions
): SpecificError[] => {
return (
specificErrorsMap
.get(errorSummary.file)
?.filter(
(specificError) =>
specificError.file === errorSummary.file &&
specificError.message === errorSummary.message &&
(errorOptions.ignoreMessages
? true
: specificError.message === errorSummary.message) &&
specificError.code === errorSummary.code
) || []
)
}

export const addHashToBaseline = (hash: string, filepath: string): void => {
const oldErrors = readTypeScriptErrorsFromFile(filepath)
const baselineErrorsFile = readBaselineErrorsFile(filepath)
if (!isBaselineVersionCurrent(baselineErrorsFile)) {
throw new Error(
'The .tsc-baseline.json is not current. Please make sure your packages are up to date and save a new baseline file'
)
}

const oldErrors = getErrorSummaryMap(baselineErrorsFile)
const newErrors = new Map<string, ErrorSummary>()

for (const [key, error] of oldErrors) {
Expand All @@ -202,5 +276,7 @@ export const addHashToBaseline = (hash: string, filepath: string): void => {
count: 1
})

writeTypeScriptErrorsToFile(newErrors, filepath)
writeTypeScriptErrorsToFile(newErrors, filepath, {
ignoreMessages: baselineErrorsFile.meta.ignoreMessages
})
}
Loading

0 comments on commit 39c4e6b

Please sign in to comment.