1
0
mirror of https://github.com/sasjs/lint.git synced 2025-12-10 09:34:34 +00:00

Compare commits

...

11 Commits

Author SHA1 Message Date
Allan Bowe
eb7f70e83a Merge pull request #215 from McGwire-Jones/add-required-macros-check
Add required macros check
2025-01-29 16:31:40 +00:00
mac.homelab
7f9ed5e61e fix: styling issue 2025-01-29 11:27:01 -05:00
mac.homelab
63255fa3c8 fix: made optionsPresent a const 2025-01-29 10:36:38 -05:00
mac.homelab
00af205a55 fix: updated content/config variable names in tests 2025-01-29 10:36:21 -05:00
mac.homelab
e74663ba54 Added example for hasRequiredMacroOptions to README 2025-01-28 10:56:26 -05:00
mac.homelab
a9cb4d8dac updated sasjslint-schema 2025-01-28 10:20:55 -05:00
McGwire-Jones
ed58b288b5 Update README.md 2025-01-28 09:57:07 -05:00
mac.homelab
be173d2e2b feat: added hasRequiredMacroOptions 2025-01-28 09:55:11 -05:00
Allan Bowe
3e4809c352 Merge pull request #214 from cjdinger/main
Add clarification about SASjs and SAS Institute
2025-01-22 21:25:57 +00:00
Chris Hemedinger
f0ab349bf7 Add clarification about SASjs and SAS Institute 2025-01-22 16:11:55 -05:00
Allan Bowe
0c5588023d Update README.md 2023-11-23 09:40:29 +00:00
7 changed files with 282 additions and 4 deletions

View File

@@ -8,6 +8,8 @@
Our goal is to help SAS developers everywhere spend less time on code reviews, bug fixing and arguing about standards - and more time delivering extraordinary business value.
*Note:* The SASjs project and its repositories are not affiliated with SAS Institute.
# Linting
@sasjs/lint is used by the following products:
@@ -252,6 +254,21 @@ 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: false
- severity: WARNING
Example
```json
{
"hasRequiredMacroOptions": true,
"requiredMacroOptions": ["SECURE", "SRC"]
}
```
## 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:
@@ -290,6 +307,12 @@ We're looking to implement the following rules:
We are also investigating some harder stuff, such as automatic indentation and code layout
# Further resources
* Using the linter on terminal: https://vid.4gl.io/w/vmJspCjcBoc5QtzwZkZRvi
* Longer intro to sasjs lint: https://vid.4gl.io/w/nDtkQFV1E8rtaa2BuM6U5s
* CLI docs: https://cli.sasjs.io/lint
# Sponsorship & Contributions
SASjs is an open source framework! Contributions are welcomed. If you would like to see a feature, because it would be useful in your project, but you don't have the requisite (Typescript) experience - then how about you engage us on a short project and we build it for you?

View File

@@ -22,7 +22,9 @@
"noTrailingSpaces": true,
"lineEndings": "off",
"strictMacroDefinition": true,
"ignoreList": ["sajsbuild", "sasjsresults"]
"ignoreList": ["sajsbuild", "sasjsresults"],
"hasRequiredMacroOptions": false,
"requiredMacroOptions": []
},
"examples": [
{
@@ -43,7 +45,9 @@
"hasMacroParentheses": true,
"lineEndings": "crlf",
"strictMacroDefinition": true,
"ignoreList": ["sajsbuild", "sasjsresults"]
"ignoreList": ["sajsbuild", "sasjsresults"],
"hasRequiredMacroOptions": false,
"requiredMacroOptions": []
}
],
"properties": {
@@ -204,6 +208,22 @@
"default": ["sasjsbuild/", "sasjsresults/"],
"examples": ["sasjs/tests", "tmp/scratch.sas"]
},
"hasRequiredMacroOptions": {
"$id": "#/properties/hasRequiredMacroOptions",
"type": "boolean",
"title": "hasRequiredMacroOptions",
"description": "Enforces required macro options as defined by requiredMacroOptions",
"default": false,
"examples": [true, false]
},
"requiredMacroOptions": {
"$id": "#/properties/requiredMacroOptions",
"type": "array",
"title": "requiredMacroOptions",
"description": "An array of macro options to require all macros to include.",
"default": [],
"examples": ["['SECURE']", "['SRC', 'STMT']"]
},
"severityLevel": {
"$id": "#/properties/severityLevel",
"type": "object",
@@ -320,6 +340,13 @@
"type": "string",
"enum": ["error", "warn"],
"default": "warn"
},
"hasRequiredMacroOptions": {
"$id": "#/properties/severityLevel/hasRequiredMacroOptions",
"title": "hasRequiredMacroOptions",
"type": "string",
"enum": ["error", "warn"],
"default": "warn"
}
}
}

View File

@@ -0,0 +1,123 @@
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 contentSecure = '%macro somemacro/ SECURE;'
const configSecure = new LintConfig({
hasRequiredMacroOptions: true,
requiredMacroOptions: ['SECURE']
})
expect(hasRequiredMacroOptions.test(contentSecure, configSecure)).toEqual(
[]
)
const contentSecureSrc = '%macro somemacro/ SECURE SRC;'
const configSecureSrc = new LintConfig({
hasRequiredMacroOptions: true,
requiredMacroOptions: ['SECURE', 'SRC']
})
expect(
hasRequiredMacroOptions.test(contentSecureSrc, configSecureSrc)
).toEqual([])
const configEmpty = new LintConfig({
hasRequiredMacroOptions: true,
requiredMacroOptions: ['']
})
expect(hasRequiredMacroOptions.test(contentSecureSrc, configEmpty)).toEqual(
[]
)
})
it('should return an array with a single diagnostic when Macro does not contain the required option', () => {
const configSecure = new LintConfig({
hasRequiredMacroOptions: true,
requiredMacroOptions: ['SECURE']
})
const contentMinXOperator = '%macro somemacro(var1, var2)/minXoperator;'
expect(
hasRequiredMacroOptions.test(contentMinXOperator, configSecure)
).toEqual([
{
message: `Macro 'somemacro' does not contain the required option 'SECURE'`,
lineNumber: 1,
startColumnNumber: 0,
endColumnNumber: 0,
severity: Severity.Warning
}
])
const contentSecureSplit = '%macro somemacro(var1, var2)/ SE CURE;'
expect(
hasRequiredMacroOptions.test(contentSecureSplit, configSecure)
).toEqual([
{
message: `Macro 'somemacro' does not contain the required option 'SECURE'`,
lineNumber: 1,
startColumnNumber: 0,
endColumnNumber: 0,
severity: Severity.Warning
}
])
const contentNoOption = '%macro somemacro(var1, var2);'
expect(hasRequiredMacroOptions.test(contentNoOption, configSecure)).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 configSrcStmt = new LintConfig({
hasRequiredMacroOptions: true,
requiredMacroOptions: ['SRC', 'STMT'],
severityLevel: { hasRequiredMacroOptions: 'warn' }
})
const contentMinXOperator = '%macro somemacro(var1, var2)/minXoperator;'
expect(
hasRequiredMacroOptions.test(contentMinXOperator, configSrcStmt)
).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 configSrcStmt = new LintConfig({
hasRequiredMacroOptions: true,
requiredMacroOptions: ['SRC', 'STMT'],
severityLevel: { hasRequiredMacroOptions: 'error' }
})
const contentSrc = '%macro somemacro(var1, var2)/ SRC;'
expect(hasRequiredMacroOptions.test(contentSrc, configSrcStmt)).toEqual([
{
message: `Macro 'somemacro' does not contain the required option 'STMT'`,
lineNumber: 1,
startColumnNumber: 0,
endColumnNumber: 0,
severity: Severity.Error
}
])
})
})

View File

@@ -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 => {
const 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
}

View File

@@ -4,3 +4,4 @@ export { hasMacroParentheses } from './hasMacroParentheses'
export { lineEndings } from './lineEndings'
export { noNestedMacros } from './noNestedMacros'
export { strictMacroDefinition } from './strictMacroDefinition'
export { hasRequiredMacroOptions } from './hasRequiredMacroOptions'

View File

@@ -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.`
)
})
})

View File

@@ -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)