From be173d2e2bc13735e92981241070206df7f6ddf6 Mon Sep 17 00:00:00 2001 From: "mac.homelab" Date: Tue, 28 Jan 2025 09:55:11 -0500 Subject: [PATCH] feat: added hasRequiredMacroOptions --- README.md | 8 ++ .../file/hasRequiredMacroOptions.spec.ts | 110 ++++++++++++++++++ src/rules/file/hasRequiredMacroOptions.ts | 52 +++++++++ src/rules/file/index.ts | 1 + src/types/LintConfig.spec.ts | 27 ++++- src/types/LintConfig.ts | 29 ++++- 6 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 src/rules/file/hasRequiredMacroOptions.spec.ts create mode 100644 src/rules/file/hasRequiredMacroOptions.ts diff --git a/README.md b/README.md index 3498610..464edd6 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,14 @@ This will highlight lines with trailing spaces. Trailing spaces serve no useful - Default: true - severity: WARNING +### hasRequiredMacroOptions + +This will require macros to have the options listed as "requiredMacroOptions." This is helpful if you want to ensure all macros are SECURE. + +- Default: true +- severity: WARNING + + ## severityLevel This setting allows the default severity to be adjusted. This is helpful when running the lint in a pipeline or git hook. Simply list the rules you would like to adjust along with the desired setting ("warn" or "error"), eg as follows: diff --git a/src/rules/file/hasRequiredMacroOptions.spec.ts b/src/rules/file/hasRequiredMacroOptions.spec.ts new file mode 100644 index 0000000..22135eb --- /dev/null +++ b/src/rules/file/hasRequiredMacroOptions.spec.ts @@ -0,0 +1,110 @@ +import { LintConfig, Severity } from '../../types' +import { hasRequiredMacroOptions } from './hasRequiredMacroOptions' + +describe('hasRequiredMacroOptions - test', () => { + it('should return an empty array when the content has the required macro option(s)', () => { + const content = '%macro somemacro/ SECURE;' + const config = new LintConfig({ + hasRequiredMacroOptions: true, + requiredMacroOptions: ['SECURE'] + }) + expect(hasRequiredMacroOptions.test(content, config)).toEqual([]) + + const content2 = '%macro somemacro/ SECURE SRC;' + const config2 = new LintConfig({ + hasRequiredMacroOptions: true, + requiredMacroOptions: ['SECURE', 'SRC'] + }) + expect(hasRequiredMacroOptions.test(content, config)).toEqual([]) + + const content3 = '%macro somemacro/ SECURE SRC;' + const config3 = new LintConfig({ + hasRequiredMacroOptions: true, + requiredMacroOptions: [''] + }) + expect(hasRequiredMacroOptions.test(content, config)).toEqual([]) + }) + + it('should return an array with a single diagnostic when Macro does not contain the required option', () => { + const config = new LintConfig({ + hasRequiredMacroOptions: true, + requiredMacroOptions: ['SECURE'] + }) + + const content = '%macro somemacro(var1, var2)/minXoperator;' + expect(hasRequiredMacroOptions.test(content, config)).toEqual([ + { + message: `Macro 'somemacro' does not contain the required option 'SECURE'`, + lineNumber: 1, + startColumnNumber: 0, + endColumnNumber: 0, + severity: Severity.Warning + } + ]) + + const content2 = '%macro somemacro(var1, var2)/ SE CURE;' + expect(hasRequiredMacroOptions.test(content2, config)).toEqual([ + { + message: `Macro 'somemacro' does not contain the required option 'SECURE'`, + lineNumber: 1, + startColumnNumber: 0, + endColumnNumber: 0, + severity: Severity.Warning + } + ]) + + const content3 = '%macro somemacro(var1, var2);' + expect(hasRequiredMacroOptions.test(content3, config)).toEqual([ + { + message: `Macro 'somemacro' does not contain the required option 'SECURE'`, + lineNumber: 1, + startColumnNumber: 0, + endColumnNumber: 0, + severity: Severity.Warning + } + ]) + }) + + it('should return an array with a two diagnostics when Macro does not contain the required options', () => { + const config = new LintConfig({ + hasRequiredMacroOptions: true, + requiredMacroOptions: ['SRC', 'STMT'], + severityLevel: { hasRequiredMacroOptions: 'warn' } + }) + const content = '%macro somemacro(var1, var2)/minXoperator;' + expect(hasRequiredMacroOptions.test(content, config)).toEqual([ + { + message: `Macro 'somemacro' does not contain the required option 'SRC'`, + lineNumber: 1, + startColumnNumber: 0, + endColumnNumber: 0, + severity: Severity.Warning + }, + { + message: `Macro 'somemacro' does not contain the required option 'STMT'`, + lineNumber: 1, + startColumnNumber: 0, + endColumnNumber: 0, + severity: Severity.Warning + } + ]) + }) + + it('should return an array with a one diagnostic when Macro contains 1 of 2 required options', () => { + const config = new LintConfig({ + hasRequiredMacroOptions: true, + requiredMacroOptions: ['SRC', 'STMT'], + severityLevel: { hasRequiredMacroOptions: 'error' } + }) + const content = '%macro somemacro(var1, var2)/ SRC;' + expect(hasRequiredMacroOptions.test(content, config)).toEqual([ + { + message: `Macro 'somemacro' does not contain the required option 'STMT'`, + lineNumber: 1, + startColumnNumber: 0, + endColumnNumber: 0, + severity: Severity.Error + } + ]) + }) +}) diff --git a/src/rules/file/hasRequiredMacroOptions.ts b/src/rules/file/hasRequiredMacroOptions.ts new file mode 100644 index 0000000..9cacc30 --- /dev/null +++ b/src/rules/file/hasRequiredMacroOptions.ts @@ -0,0 +1,52 @@ +import { Diagnostic, LintConfig, Macro, Severity } from '../../types' +import { FileLintRule } from '../../types/LintRule' +import { LintRuleType } from '../../types/LintRuleType' +import { parseMacros } from '../../utils/parseMacros' + +const name = 'hasRequiredMacroOptions' +const description = 'Enforce required macro options' +const message = 'Macro defined without required options' + +const processOptions = ( + macro: Macro, + diagnostics: Diagnostic[], + config?: LintConfig +): void => { + let optionsPresent = macro.declaration.split('/')?.[1]?.trim() ?? '' + const severity = config?.severityLevel[name] || Severity.Warning + + config?.requiredMacroOptions.forEach((option) => { + if (!optionsPresent.includes(option)) { + diagnostics.push({ + message: `Macro '${macro.name}' does not contain the required option '${option}'`, + lineNumber: macro.startLineNumbers[0], + startColumnNumber: 0, + endColumnNumber: 0, + severity + }) + } + }) +} + +const test = (value: string, config?: LintConfig) => { + const diagnostics: Diagnostic[] = [] + + const macros = parseMacros(value, config) + + macros.forEach((macro) => { + processOptions(macro, diagnostics, config) + }) + + return diagnostics +} + +/** + * Lint rule that checks if a macro has the required options + */ +export const hasRequiredMacroOptions: FileLintRule = { + type: LintRuleType.File, + name, + description, + message, + test +} diff --git a/src/rules/file/index.ts b/src/rules/file/index.ts index b3a13b7..cbe3a00 100644 --- a/src/rules/file/index.ts +++ b/src/rules/file/index.ts @@ -4,3 +4,4 @@ export { hasMacroParentheses } from './hasMacroParentheses' export { lineEndings } from './lineEndings' export { noNestedMacros } from './noNestedMacros' export { strictMacroDefinition } from './strictMacroDefinition' +export { hasRequiredMacroOptions } from './hasRequiredMacroOptions' diff --git a/src/types/LintConfig.spec.ts b/src/types/LintConfig.spec.ts index c7abef8..93e5ba3 100644 --- a/src/types/LintConfig.spec.ts +++ b/src/types/LintConfig.spec.ts @@ -1,3 +1,4 @@ +import { hasRequiredMacroOptions } from '../rules/file' import { LineEndings } from './LineEndings' import { LintConfig } from './LintConfig' import { LintRuleType } from './LintRuleType' @@ -168,6 +169,7 @@ describe('LintConfig', () => { hasMacroNameInMend: true, noNestedMacros: true, hasMacroParentheses: true, + hasRequiredMacroOptions: true, noGremlins: true, lineEndings: 'lf' }) @@ -187,7 +189,7 @@ describe('LintConfig', () => { expect(config.lineLintRules[5].name).toEqual('noGremlins') expect(config.lineLintRules[5].type).toEqual(LintRuleType.Line) - expect(config.fileLintRules.length).toEqual(6) + expect(config.fileLintRules.length).toEqual(7) expect(config.fileLintRules[0].name).toEqual('lineEndings') expect(config.fileLintRules[0].type).toEqual(LintRuleType.File) expect(config.fileLintRules[1].name).toEqual('hasDoxygenHeader') @@ -200,6 +202,8 @@ describe('LintConfig', () => { expect(config.fileLintRules[4].type).toEqual(LintRuleType.File) expect(config.fileLintRules[5].name).toEqual('strictMacroDefinition') expect(config.fileLintRules[5].type).toEqual(LintRuleType.File) + expect(config.fileLintRules[6].name).toEqual('hasRequiredMacroOptions') + expect(config.fileLintRules[6].type).toEqual(LintRuleType.File) expect(config.pathLintRules.length).toEqual(2) expect(config.pathLintRules[0].name).toEqual('noSpacesInFileNames') @@ -207,4 +211,25 @@ describe('LintConfig', () => { expect(config.pathLintRules[1].name).toEqual('lowerCaseFileNames') expect(config.pathLintRules[1].type).toEqual(LintRuleType.Path) }) + + it('should throw an error with an invalid value for requiredMacroOptions', () => { + expect( + () => + new LintConfig({ + hasRequiredMacroOptions: true, + requiredMacroOptions: 'test' + }) + ).toThrowError( + `Property "requiredMacroOptions" can only be an array of strings.` + ) + expect( + () => + new LintConfig({ + hasRequiredMacroOptions: true, + requiredMacroOptions: ['test', 2] + }) + ).toThrowError( + `Property "requiredMacroOptions" has invalid type of values. It can only contain strings.` + ) + }) }) diff --git a/src/types/LintConfig.ts b/src/types/LintConfig.ts index e77f7c5..dce2c04 100644 --- a/src/types/LintConfig.ts +++ b/src/types/LintConfig.ts @@ -4,7 +4,8 @@ import { noNestedMacros, hasMacroParentheses, lineEndings, - strictMacroDefinition + strictMacroDefinition, + hasRequiredMacroOptions } from '../rules/file' import { indentationMultiple, @@ -40,6 +41,7 @@ export class LintConfig { readonly lineEndings: LineEndings = LineEndings.LF readonly defaultHeader: string = getDefaultHeader() readonly severityLevel: { [key: string]: Severity } = {} + readonly requiredMacroOptions: string[] = [] constructor(json?: any) { if (json?.ignoreList) { @@ -132,6 +134,31 @@ export class LintConfig { this.fileLintRules.push(strictMacroDefinition) } + if (json?.hasRequiredMacroOptions) { + this.fileLintRules.push(hasRequiredMacroOptions) + + if (json?.requiredMacroOptions) { + if ( + Array.isArray(json.requiredMacroOptions) && + json.requiredMacroOptions.length > 0 + ) { + json.requiredMacroOptions.forEach((item: any) => { + if (typeof item === 'string') { + this.requiredMacroOptions.push(item) + } else { + throw new Error( + `Property "requiredMacroOptions" has invalid type of values. It can only contain strings.` + ) + } + }) + } else { + throw new Error( + `Property "requiredMacroOptions" can only be an array of strings.` + ) + } + } + } + if (json?.noGremlins !== false) { this.lineLintRules.push(noGremlins)