mirror of
https://github.com/sasjs/lint.git
synced 2025-12-10 17:34:36 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8be59ac591 | ||
|
|
c6a70a1d1a | ||
| 75b103003c | |||
| 0cff87fe12 |
37
README.md
37
README.md
@@ -40,9 +40,19 @@ Configuration is via a `.sasjslint` file with the following structure (these are
|
|||||||
|
|
||||||
### SAS Lint Settings
|
### SAS Lint Settings
|
||||||
|
|
||||||
|
Each setting can have three states:
|
||||||
|
|
||||||
|
* OFF - usually by setting the value to `false` or 0. In this case, the rule won't be executed.
|
||||||
|
* WARN - a warning is written to the log, but the return code will be 0
|
||||||
|
* ERROR - an error is written to the log, and the return code is 1
|
||||||
|
|
||||||
|
For more details, and the default state, see the description of each rule below. It is also possible to change whether a rule returns ERROR or WARN using the `severityLevels` object.
|
||||||
|
|
||||||
#### defaultHeader
|
#### defaultHeader
|
||||||
|
|
||||||
This sets the default program header - applies when a SAS program does NOT begin with `/**`. The default header is as follows:
|
This isn't actually a rule - but rather a formatting setting, which applies to SAS program that do NOT begin with `/**`. It can be triggered by running `sasjs lint fix` in the SASjs CLI, or by hitting "save" when using the SASjs VS Code extension (with "formatOnSave" in place)
|
||||||
|
|
||||||
|
The default header is as follows:
|
||||||
|
|
||||||
```sas
|
```sas
|
||||||
/**
|
/**
|
||||||
@@ -51,8 +61,7 @@ This sets the default program header - applies when a SAS program does NOT begin
|
|||||||
<h4> SAS Macros </h4>
|
<h4> SAS Macros </h4>
|
||||||
**/
|
**/
|
||||||
```
|
```
|
||||||
|
If creating a new value, use `{lineEnding}` instead of `\n`, eg as follows:
|
||||||
The default header is automatically applied when running `sasjs lint fix` in the SASjs CLI, or by hitting "save" when using the SASjs VS Code extension. If creating a new value, use `{lineEnding}` instead of `\n`, eg as follows:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -62,7 +71,7 @@ The default header is automatically applied when running `sasjs lint fix` in the
|
|||||||
|
|
||||||
#### noEncodedPasswords
|
#### noEncodedPasswords
|
||||||
|
|
||||||
This will highlight any rows that contain a `{sas00X}` type password, or `{sasenc}`. These passwords (especially 001 and 002) are NOT secure, and should NEVER be pushed to source control or saved to the filesystem without special permissions applied.
|
This rule will highlight any rows that contain a `{sas00X}` type password, or `{sasenc}`. These passwords (especially 001 and 002) are NOT secure, and should NEVER be pushed to source control or saved to the filesystem without special permissions applied.
|
||||||
|
|
||||||
* Default: true
|
* Default: true
|
||||||
* Severity: ERROR
|
* Severity: ERROR
|
||||||
@@ -139,6 +148,26 @@ This will highlight lines with trailing spaces. Trailing spaces serve no useful
|
|||||||
* Default: true
|
* Default: true
|
||||||
* severity: WARNING
|
* 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:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"noTrailingSpaces": true,
|
||||||
|
"hasDoxygenHeader": true,
|
||||||
|
"maxLineLength": 100,
|
||||||
|
"severityLevel": {
|
||||||
|
"hasDoxygenHeader": "warn",
|
||||||
|
"maxLineLength": "error",
|
||||||
|
"noTrailingSpaces": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* "warn" - show warning in the log (doesn’t affect exit code)
|
||||||
|
* "error" - show error in the log (exit code is 1 when triggered)
|
||||||
|
|
||||||
### Upcoming Linting Rules:
|
### Upcoming Linting Rules:
|
||||||
|
|
||||||
* `noTabs` -> does what it says on the tin
|
* `noTabs` -> does what it says on the tin
|
||||||
|
|||||||
@@ -159,6 +159,115 @@
|
|||||||
"description": "An array of paths or path patterns to ignore when linting. Any files or matching patterns in the .gitignore file will also be ignored.",
|
"description": "An array of paths or path patterns to ignore when linting. Any files or matching patterns in the .gitignore file will also be ignored.",
|
||||||
"default": ["sasjsbuild/", "sasjsresults/"],
|
"default": ["sasjsbuild/", "sasjsresults/"],
|
||||||
"examples": ["sasjs/tests", "tmp/scratch.sas"]
|
"examples": ["sasjs/tests", "tmp/scratch.sas"]
|
||||||
|
},
|
||||||
|
"severityLevel": {
|
||||||
|
"$id": "#/properties/severityLevel",
|
||||||
|
"type": "object",
|
||||||
|
"title": "severityLevel",
|
||||||
|
"description": "An object which specifies the severity level of each rule.",
|
||||||
|
"default": {},
|
||||||
|
"examples": [{
|
||||||
|
"hasDoxygenHeader": "warn",
|
||||||
|
"maxLineLength": "warn",
|
||||||
|
"noTrailingSpaces": "error"
|
||||||
|
}, {
|
||||||
|
"hasDoxygenHeader": "warn",
|
||||||
|
"maxLineLength": "error",
|
||||||
|
"noTrailingSpaces": "error"
|
||||||
|
}],
|
||||||
|
"properties": {
|
||||||
|
"noEncodedPasswords": {
|
||||||
|
"$id": "#/properties/severityLevel/noEncodedPasswords",
|
||||||
|
"title": "noEncodedPasswords",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["error", "warn"],
|
||||||
|
"default": "error"
|
||||||
|
},
|
||||||
|
"hasDoxygenHeader": {
|
||||||
|
"$id": "#/properties/severityLevel/hasDoxygenHeader",
|
||||||
|
"title": "hasDoxygenHeader",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["error", "warn"],
|
||||||
|
"default": "warn" },
|
||||||
|
"hasMacroNameInMend": {
|
||||||
|
"$id": "#/properties/severityLevel/hasMacroNameInMend",
|
||||||
|
"title": "hasMacroNameInMend",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["error", "warn"],
|
||||||
|
"default": "warn"
|
||||||
|
},
|
||||||
|
"hasMacroParentheses": {
|
||||||
|
"$id": "#/properties/severityLevel/hasMacroParentheses",
|
||||||
|
"title": "hasMacroParentheses",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["error", "warn"],
|
||||||
|
"default": "warn"
|
||||||
|
},
|
||||||
|
"indentationMultiple": {
|
||||||
|
"$id": "#/properties/severityLevel/indentationMultiple",
|
||||||
|
"title": "indentationMultiple",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["error", "warn"],
|
||||||
|
"default": "warn"
|
||||||
|
},
|
||||||
|
"lowerCaseFileNames": {
|
||||||
|
"$id": "#/properties/severityLevel/lowerCaseFileNames",
|
||||||
|
"title": "lowerCaseFileNames",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["error", "warn"],
|
||||||
|
"default": "warn"
|
||||||
|
},
|
||||||
|
"maxLineLength": {
|
||||||
|
"$id": "#/properties/severityLevel/maxLineLength",
|
||||||
|
"title": "maxLineLength",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["error", "warn"],
|
||||||
|
"default": "warn"
|
||||||
|
},
|
||||||
|
"noNestedMacros": {
|
||||||
|
"$id": "#/properties/severityLevel/noNestedMacros",
|
||||||
|
"title": "noNestedMacros",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["error", "warn"],
|
||||||
|
"default": "warn"
|
||||||
|
},
|
||||||
|
"noSpacesInFileNames": {
|
||||||
|
"$id": "#/properties/severityLevel/noSpacesInFileNames",
|
||||||
|
"title": "noSpacesInFileNames",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["error", "warn"],
|
||||||
|
"default": "warn"
|
||||||
|
},
|
||||||
|
"noTabIndentation": {
|
||||||
|
"$id": "#/properties/severityLevel/noTabIndentation",
|
||||||
|
"title": "noTabIndentation",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["error", "warn"],
|
||||||
|
"default": "warn"
|
||||||
|
},
|
||||||
|
"noTrailingSpaces": {
|
||||||
|
"$id": "#/properties/severityLevel/noTrailingSpaces",
|
||||||
|
"title": "noTrailingSpaces",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["error", "warn"],
|
||||||
|
"default": "warn"
|
||||||
|
},
|
||||||
|
"lineEndings": {
|
||||||
|
"$id": "#/properties/severityLevel/lineEndings",
|
||||||
|
"title": "lineEndings",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["error", "warn"],
|
||||||
|
"default": "warn"
|
||||||
|
},
|
||||||
|
"strictMacroDefinition": {
|
||||||
|
"$id": "#/properties/severityLevel/strictMacroDefinition",
|
||||||
|
"title": "strictMacroDefinition",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["error", "warn"],
|
||||||
|
"default": "warn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const processFile = (
|
|||||||
): Diagnostic[] => {
|
): Diagnostic[] => {
|
||||||
const diagnostics: Diagnostic[] = []
|
const diagnostics: Diagnostic[] = []
|
||||||
config.pathLintRules.forEach((rule) => {
|
config.pathLintRules.forEach((rule) => {
|
||||||
diagnostics.push(...rule.test(filePath))
|
diagnostics.push(...rule.test(filePath, config))
|
||||||
})
|
})
|
||||||
|
|
||||||
return diagnostics
|
return diagnostics
|
||||||
@@ -27,7 +27,7 @@ export const processFile = (
|
|||||||
const processContent = (config: LintConfig, content: string): Diagnostic[] => {
|
const processContent = (config: LintConfig, content: string): Diagnostic[] => {
|
||||||
const diagnostics: Diagnostic[] = []
|
const diagnostics: Diagnostic[] = []
|
||||||
config.fileLintRules.forEach((rule) => {
|
config.fileLintRules.forEach((rule) => {
|
||||||
diagnostics.push(...rule.test(content))
|
diagnostics.push(...rule.test(content, config))
|
||||||
})
|
})
|
||||||
|
|
||||||
return diagnostics
|
return diagnostics
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ const description =
|
|||||||
const message = 'File missing Doxygen header'
|
const message = 'File missing Doxygen header'
|
||||||
const messageForSingleAsterisk =
|
const messageForSingleAsterisk =
|
||||||
'File not following Doxygen header style, use double asterisks'
|
'File not following Doxygen header style, use double asterisks'
|
||||||
|
|
||||||
const test = (value: string, config?: LintConfig) => {
|
const test = (value: string, config?: LintConfig) => {
|
||||||
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
|
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
|
||||||
|
const severity = config?.severityLevel[name] || Severity.Warning
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const hasFileHeader = value.trimStart().startsWith('/**')
|
const hasFileHeader = value.trimStart().startsWith('/**')
|
||||||
if (hasFileHeader) return []
|
if (hasFileHeader) return []
|
||||||
@@ -27,7 +30,7 @@ const test = (value: string, config?: LintConfig) => {
|
|||||||
.length + 1,
|
.length + 1,
|
||||||
startColumnNumber: 1,
|
startColumnNumber: 1,
|
||||||
endColumnNumber: 1,
|
endColumnNumber: 1,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -37,7 +40,7 @@ const test = (value: string, config?: LintConfig) => {
|
|||||||
lineNumber: 1,
|
lineNumber: 1,
|
||||||
startColumnNumber: 1,
|
startColumnNumber: 1,
|
||||||
endColumnNumber: 1,
|
endColumnNumber: 1,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -47,7 +50,7 @@ const test = (value: string, config?: LintConfig) => {
|
|||||||
lineNumber: 1,
|
lineNumber: 1,
|
||||||
startColumnNumber: 1,
|
startColumnNumber: 1,
|
||||||
endColumnNumber: 1,
|
endColumnNumber: 1,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,14 @@ const name = 'hasMacroNameInMend'
|
|||||||
const description =
|
const description =
|
||||||
'Enforces the presence of the macro name in each %mend statement.'
|
'Enforces the presence of the macro name in each %mend statement.'
|
||||||
const message = '%mend statement has missing or incorrect macro name'
|
const message = '%mend statement has missing or incorrect macro name'
|
||||||
|
|
||||||
const test = (value: string, config?: LintConfig) => {
|
const test = (value: string, config?: LintConfig) => {
|
||||||
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
|
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
|
||||||
const lines: string[] = value ? value.split(lineEnding) : []
|
const lines: string[] = value ? value.split(lineEnding) : []
|
||||||
const macros = parseMacros(value, config)
|
const macros = parseMacros(value, config)
|
||||||
|
const severity = config?.severityLevel[name] || Severity.Warning
|
||||||
const diagnostics: Diagnostic[] = []
|
const diagnostics: Diagnostic[] = []
|
||||||
|
|
||||||
macros.forEach((macro) => {
|
macros.forEach((macro) => {
|
||||||
if (macro.startLineNumbers.length === 0 && macro.endLineNumber !== null) {
|
if (macro.startLineNumbers.length === 0 && macro.endLineNumber !== null) {
|
||||||
const endLine = lines[macro.endLineNumber - 1]
|
const endLine = lines[macro.endLineNumber - 1]
|
||||||
@@ -25,7 +28,7 @@ const test = (value: string, config?: LintConfig) => {
|
|||||||
startColumnNumber: getColumnNumber(endLine, '%mend'),
|
startColumnNumber: getColumnNumber(endLine, '%mend'),
|
||||||
endColumnNumber:
|
endColumnNumber:
|
||||||
getColumnNumber(endLine, '%mend') + macro.termination.length,
|
getColumnNumber(endLine, '%mend') + macro.termination.length,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
})
|
})
|
||||||
} else if (
|
} else if (
|
||||||
macro.endLineNumber === null &&
|
macro.endLineNumber === null &&
|
||||||
@@ -36,7 +39,7 @@ const test = (value: string, config?: LintConfig) => {
|
|||||||
lineNumber: macro.startLineNumbers![0],
|
lineNumber: macro.startLineNumbers![0],
|
||||||
startColumnNumber: 1,
|
startColumnNumber: 1,
|
||||||
endColumnNumber: 1,
|
endColumnNumber: 1,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
})
|
})
|
||||||
} else if (macro.mismatchedMendMacroName) {
|
} else if (macro.mismatchedMendMacroName) {
|
||||||
const endLine = lines[(macro.endLineNumber as number) - 1]
|
const endLine = lines[(macro.endLineNumber as number) - 1]
|
||||||
@@ -53,7 +56,7 @@ const test = (value: string, config?: LintConfig) => {
|
|||||||
getColumnNumber(endLine, macro.mismatchedMendMacroName) +
|
getColumnNumber(endLine, macro.mismatchedMendMacroName) +
|
||||||
macro.mismatchedMendMacroName.length -
|
macro.mismatchedMendMacroName.length -
|
||||||
1,
|
1,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
})
|
})
|
||||||
} else if (!macro.hasMacroNameInMend) {
|
} else if (!macro.hasMacroNameInMend) {
|
||||||
const endLine = lines[(macro.endLineNumber as number) - 1]
|
const endLine = lines[(macro.endLineNumber as number) - 1]
|
||||||
@@ -62,7 +65,7 @@ const test = (value: string, config?: LintConfig) => {
|
|||||||
lineNumber: macro.endLineNumber as number,
|
lineNumber: macro.endLineNumber as number,
|
||||||
startColumnNumber: getColumnNumber(endLine, '%mend'),
|
startColumnNumber: getColumnNumber(endLine, '%mend'),
|
||||||
endColumnNumber: getColumnNumber(endLine, '%mend') + 6,
|
endColumnNumber: getColumnNumber(endLine, '%mend') + 6,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ import { LintConfig } from '../../types'
|
|||||||
const name = 'hasMacroParentheses'
|
const name = 'hasMacroParentheses'
|
||||||
const description = 'Enforces the presence of parentheses in macro definitions.'
|
const description = 'Enforces the presence of parentheses in macro definitions.'
|
||||||
const message = 'Macro definition missing parentheses'
|
const message = 'Macro definition missing parentheses'
|
||||||
|
|
||||||
const test = (value: string, config?: LintConfig) => {
|
const test = (value: string, config?: LintConfig) => {
|
||||||
const diagnostics: Diagnostic[] = []
|
const diagnostics: Diagnostic[] = []
|
||||||
const macros = parseMacros(value, config)
|
const macros = parseMacros(value, config)
|
||||||
|
const severity = config?.severityLevel[name] || Severity.Warning
|
||||||
|
|
||||||
macros.forEach((macro) => {
|
macros.forEach((macro) => {
|
||||||
if (!macro.name) {
|
if (!macro.name) {
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
@@ -24,7 +27,7 @@ const test = (value: string, config?: LintConfig) => {
|
|||||||
endColumnNumber:
|
endColumnNumber:
|
||||||
getColumnNumber(macro.declarationLines![0], '%macro') +
|
getColumnNumber(macro.declarationLines![0], '%macro') +
|
||||||
macro.declaration.length,
|
macro.declaration.length,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
})
|
})
|
||||||
} else if (!macro.declarationLines.find((dl) => dl.includes('('))) {
|
} else if (!macro.declarationLines.find((dl) => dl.includes('('))) {
|
||||||
const macroNameLineIndex = macro.declarationLines.findIndex((dl) =>
|
const macroNameLineIndex = macro.declarationLines.findIndex((dl) =>
|
||||||
@@ -44,7 +47,7 @@ const test = (value: string, config?: LintConfig) => {
|
|||||||
) +
|
) +
|
||||||
macro.name.length -
|
macro.name.length -
|
||||||
1,
|
1,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Severity } from '../../types/Severity'
|
|||||||
const name = 'lineEndings'
|
const name = 'lineEndings'
|
||||||
const description = 'Ensures line endings conform to the configured type.'
|
const description = 'Ensures line endings conform to the configured type.'
|
||||||
const message = 'Incorrect line ending - {actual} instead of {expected}'
|
const message = 'Incorrect line ending - {actual} instead of {expected}'
|
||||||
|
|
||||||
const test = (value: string, config?: LintConfig) => {
|
const test = (value: string, config?: LintConfig) => {
|
||||||
const lineEndingConfig = config?.lineEndings || LineEndings.LF
|
const lineEndingConfig = config?.lineEndings || LineEndings.LF
|
||||||
const expectedLineEnding =
|
const expectedLineEnding =
|
||||||
@@ -18,8 +19,10 @@ const test = (value: string, config?: LintConfig) => {
|
|||||||
.replace(/\n/g, '{lf}')
|
.replace(/\n/g, '{lf}')
|
||||||
.split(new RegExp(`(?<=${expectedLineEnding})`))
|
.split(new RegExp(`(?<=${expectedLineEnding})`))
|
||||||
const diagnostics: Diagnostic[] = []
|
const diagnostics: Diagnostic[] = []
|
||||||
|
const severity = config?.severityLevel[name] || Severity.Warning
|
||||||
|
|
||||||
let indexOffset = 0
|
let indexOffset = 0
|
||||||
|
|
||||||
lines.forEach((line, index) => {
|
lines.forEach((line, index) => {
|
||||||
if (line.endsWith(incorrectLineEnding)) {
|
if (line.endsWith(incorrectLineEnding)) {
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
@@ -29,7 +32,7 @@ const test = (value: string, config?: LintConfig) => {
|
|||||||
lineNumber: index + 1 + indexOffset,
|
lineNumber: index + 1 + indexOffset,
|
||||||
startColumnNumber: line.indexOf(incorrectLineEnding),
|
startColumnNumber: line.indexOf(incorrectLineEnding),
|
||||||
endColumnNumber: line.indexOf(incorrectLineEnding) + 1,
|
endColumnNumber: line.indexOf(incorrectLineEnding) + 1,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const splitLine = line.split(new RegExp(`(?<=${incorrectLineEnding})`))
|
const splitLine = line.split(new RegExp(`(?<=${incorrectLineEnding})`))
|
||||||
@@ -51,7 +54,7 @@ const test = (value: string, config?: LintConfig) => {
|
|||||||
lineNumber: index + i + 1,
|
lineNumber: index + i + 1,
|
||||||
startColumnNumber: l.indexOf(incorrectLineEnding),
|
startColumnNumber: l.indexOf(incorrectLineEnding),
|
||||||
endColumnNumber: l.indexOf(incorrectLineEnding) + 1,
|
endColumnNumber: l.indexOf(incorrectLineEnding) + 1,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,11 +10,14 @@ import { LineEndings } from '../../types/LineEndings'
|
|||||||
const name = 'noNestedMacros'
|
const name = 'noNestedMacros'
|
||||||
const description = 'Enfoces the absence of nested macro definitions.'
|
const description = 'Enfoces the absence of nested macro definitions.'
|
||||||
const message = `Macro definition for '{macro}' present in macro '{parent}'`
|
const message = `Macro definition for '{macro}' present in macro '{parent}'`
|
||||||
|
|
||||||
const test = (value: string, config?: LintConfig) => {
|
const test = (value: string, config?: LintConfig) => {
|
||||||
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
|
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
|
||||||
const lines: string[] = value ? value.split(lineEnding) : []
|
const lines: string[] = value ? value.split(lineEnding) : []
|
||||||
const diagnostics: Diagnostic[] = []
|
const diagnostics: Diagnostic[] = []
|
||||||
const macros = parseMacros(value, config)
|
const macros = parseMacros(value, config)
|
||||||
|
const severity = config?.severityLevel[name] || Severity.Warning
|
||||||
|
|
||||||
macros
|
macros
|
||||||
.filter((m) => !!m.parentMacro)
|
.filter((m) => !!m.parentMacro)
|
||||||
.forEach((macro) => {
|
.forEach((macro) => {
|
||||||
@@ -34,7 +37,7 @@ const test = (value: string, config?: LintConfig) => {
|
|||||||
) +
|
) +
|
||||||
lines[(macro.startLineNumbers![0] as number) - 1].trim().length -
|
lines[(macro.startLineNumbers![0] as number) - 1].trim().length -
|
||||||
1,
|
1,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return diagnostics
|
return diagnostics
|
||||||
|
|||||||
@@ -25,9 +25,11 @@ const validOptions = [
|
|||||||
const processParams = (
|
const processParams = (
|
||||||
content: string,
|
content: string,
|
||||||
macro: Macro,
|
macro: Macro,
|
||||||
diagnostics: Diagnostic[]
|
diagnostics: Diagnostic[],
|
||||||
|
config?: LintConfig
|
||||||
): string => {
|
): string => {
|
||||||
const declaration = macro.declaration
|
const declaration = macro.declaration
|
||||||
|
const severity = config?.severityLevel[name] || Severity.Warning
|
||||||
|
|
||||||
const regExpParams = new RegExp(/(?<=\().*(?=\))/)
|
const regExpParams = new RegExp(/(?<=\().*(?=\))/)
|
||||||
const regExpParamsResult = regExpParams.exec(declaration)
|
const regExpParamsResult = regExpParams.exec(declaration)
|
||||||
@@ -88,7 +90,7 @@ const processParams = (
|
|||||||
lineNumber: paramLineNumber,
|
lineNumber: paramLineNumber,
|
||||||
startColumnNumber: paramStartIndex + 1,
|
startColumnNumber: paramStartIndex + 1,
|
||||||
endColumnNumber: paramEndIndex,
|
endColumnNumber: paramEndIndex,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -101,9 +103,11 @@ const processParams = (
|
|||||||
const processOptions = (
|
const processOptions = (
|
||||||
_declaration: string,
|
_declaration: string,
|
||||||
macro: Macro,
|
macro: Macro,
|
||||||
diagnostics: Diagnostic[]
|
diagnostics: Diagnostic[],
|
||||||
|
config?: LintConfig
|
||||||
): void => {
|
): void => {
|
||||||
let optionsPresent = _declaration.split('/')?.[1]?.trim()
|
let optionsPresent = _declaration.split('/')?.[1]?.trim()
|
||||||
|
const severity = config?.severityLevel[name] || Severity.Warning
|
||||||
|
|
||||||
if (optionsPresent) {
|
if (optionsPresent) {
|
||||||
const regex = new RegExp(/="(.*?)"/, 'g')
|
const regex = new RegExp(/="(.*?)"/, 'g')
|
||||||
@@ -136,7 +140,7 @@ const processOptions = (
|
|||||||
startColumnNumber: declarationLine.indexOf(trimmedOption) + 1,
|
startColumnNumber: declarationLine.indexOf(trimmedOption) + 1,
|
||||||
endColumnNumber:
|
endColumnNumber:
|
||||||
declarationLine.indexOf(trimmedOption) + trimmedOption.length,
|
declarationLine.indexOf(trimmedOption) + trimmedOption.length,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -149,9 +153,9 @@ const test = (value: string, config?: LintConfig) => {
|
|||||||
const macros = parseMacros(value, config)
|
const macros = parseMacros(value, config)
|
||||||
|
|
||||||
macros.forEach((macro) => {
|
macros.forEach((macro) => {
|
||||||
const _declaration = processParams(value, macro, diagnostics)
|
const _declaration = processParams(value, macro, diagnostics, config)
|
||||||
|
|
||||||
processOptions(_declaration, macro, diagnostics)
|
processOptions(_declaration, macro, diagnostics, config)
|
||||||
})
|
})
|
||||||
|
|
||||||
return diagnostics
|
return diagnostics
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import { Severity } from '../../types/Severity'
|
|||||||
const name = 'indentationMultiple'
|
const name = 'indentationMultiple'
|
||||||
const description = 'Ensure indentation by a multiple of the configured number.'
|
const description = 'Ensure indentation by a multiple of the configured number.'
|
||||||
const message = 'Line has incorrect indentation'
|
const message = 'Line has incorrect indentation'
|
||||||
|
|
||||||
const test = (value: string, lineNumber: number, config?: LintConfig) => {
|
const test = (value: string, lineNumber: number, config?: LintConfig) => {
|
||||||
if (!value.startsWith(' ')) return []
|
if (!value.startsWith(' ')) return []
|
||||||
|
|
||||||
|
const severity = config?.severityLevel[name] || Severity.Warning
|
||||||
const indentationMultiple = isNaN(config?.indentationMultiple as number)
|
const indentationMultiple = isNaN(config?.indentationMultiple as number)
|
||||||
? 2
|
? 2
|
||||||
: config!.indentationMultiple
|
: config!.indentationMultiple
|
||||||
@@ -24,7 +26,7 @@ const test = (value: string, lineNumber: number, config?: LintConfig) => {
|
|||||||
lineNumber,
|
lineNumber,
|
||||||
startColumnNumber: 1,
|
startColumnNumber: 1,
|
||||||
endColumnNumber: 1,
|
endColumnNumber: 1,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import { Severity } from '../../types/Severity'
|
|||||||
const name = 'maxLineLength'
|
const name = 'maxLineLength'
|
||||||
const description = 'Restrict lines to the specified length.'
|
const description = 'Restrict lines to the specified length.'
|
||||||
const message = 'Line exceeds maximum length'
|
const message = 'Line exceeds maximum length'
|
||||||
|
|
||||||
const test = (value: string, lineNumber: number, config?: LintConfig) => {
|
const test = (value: string, lineNumber: number, config?: LintConfig) => {
|
||||||
|
const severity = config?.severityLevel[name] || Severity.Warning
|
||||||
const maxLineLength = config?.maxLineLength || 80
|
const maxLineLength = config?.maxLineLength || 80
|
||||||
if (value.length <= maxLineLength) return []
|
if (value.length <= maxLineLength) return []
|
||||||
return [
|
return [
|
||||||
@@ -15,7 +17,7 @@ const test = (value: string, lineNumber: number, config?: LintConfig) => {
|
|||||||
lineNumber,
|
lineNumber,
|
||||||
startColumnNumber: 1,
|
startColumnNumber: 1,
|
||||||
endColumnNumber: 1,
|
endColumnNumber: 1,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LintConfig } from '../../types'
|
||||||
import { LineLintRule } from '../../types/LintRule'
|
import { LineLintRule } from '../../types/LintRule'
|
||||||
import { LintRuleType } from '../../types/LintRuleType'
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
import { Severity } from '../../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
@@ -5,7 +6,9 @@ import { Severity } from '../../types/Severity'
|
|||||||
const name = 'noEncodedPasswords'
|
const name = 'noEncodedPasswords'
|
||||||
const description = 'Disallow encoded passwords in SAS code.'
|
const description = 'Disallow encoded passwords in SAS code.'
|
||||||
const message = 'Line contains encoded password'
|
const message = 'Line contains encoded password'
|
||||||
const test = (value: string, lineNumber: number) => {
|
|
||||||
|
const test = (value: string, lineNumber: number, config?: LintConfig) => {
|
||||||
|
const severity = config?.severityLevel[name] || Severity.Error
|
||||||
const regex = new RegExp(/{sas(\d{2,4}|enc)}[^;"'\s]*/, 'gi')
|
const regex = new RegExp(/{sas(\d{2,4}|enc)}[^;"'\s]*/, 'gi')
|
||||||
const matches = value.match(regex)
|
const matches = value.match(regex)
|
||||||
if (!matches || !matches.length) return []
|
if (!matches || !matches.length) return []
|
||||||
@@ -14,7 +17,7 @@ const test = (value: string, lineNumber: number) => {
|
|||||||
lineNumber,
|
lineNumber,
|
||||||
startColumnNumber: value.indexOf(match) + 1,
|
startColumnNumber: value.indexOf(match) + 1,
|
||||||
endColumnNumber: value.indexOf(match) + match.length + 1,
|
endColumnNumber: value.indexOf(match) + match.length + 1,
|
||||||
severity: Severity.Error
|
severity
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LintConfig } from '../../types'
|
||||||
import { LineLintRule } from '../../types/LintRule'
|
import { LineLintRule } from '../../types/LintRule'
|
||||||
import { LintRuleType } from '../../types/LintRuleType'
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
import { Severity } from '../../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
@@ -5,7 +6,9 @@ import { Severity } from '../../types/Severity'
|
|||||||
const name = 'noTabs'
|
const name = 'noTabs'
|
||||||
const description = 'Disallow indenting with tabs.'
|
const description = 'Disallow indenting with tabs.'
|
||||||
const message = 'Line is indented with a tab'
|
const message = 'Line is indented with a tab'
|
||||||
const test = (value: string, lineNumber: number) => {
|
|
||||||
|
const test = (value: string, lineNumber: number, config?: LintConfig) => {
|
||||||
|
const severity = config?.severityLevel[name] || Severity.Warning
|
||||||
if (!value.startsWith('\t')) return []
|
if (!value.startsWith('\t')) return []
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -13,7 +16,7 @@ const test = (value: string, lineNumber: number) => {
|
|||||||
lineNumber,
|
lineNumber,
|
||||||
startColumnNumber: 1,
|
startColumnNumber: 1,
|
||||||
endColumnNumber: 1,
|
endColumnNumber: 1,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LintConfig } from '../../types'
|
||||||
import { LineLintRule } from '../../types/LintRule'
|
import { LineLintRule } from '../../types/LintRule'
|
||||||
import { LintRuleType } from '../../types/LintRuleType'
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
import { Severity } from '../../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
@@ -5,8 +6,11 @@ import { Severity } from '../../types/Severity'
|
|||||||
const name = 'noTrailingSpaces'
|
const name = 'noTrailingSpaces'
|
||||||
const description = 'Disallow trailing spaces on lines.'
|
const description = 'Disallow trailing spaces on lines.'
|
||||||
const message = 'Line contains trailing spaces'
|
const message = 'Line contains trailing spaces'
|
||||||
const test = (value: string, lineNumber: number) =>
|
|
||||||
value.trimEnd() === value
|
const test = (value: string, lineNumber: number, config?: LintConfig) => {
|
||||||
|
const severity = config?.severityLevel[name] || Severity.Warning
|
||||||
|
|
||||||
|
return value.trimEnd() === value
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
@@ -14,9 +18,11 @@ const test = (value: string, lineNumber: number) =>
|
|||||||
lineNumber,
|
lineNumber,
|
||||||
startColumnNumber: value.trimEnd().length + 1,
|
startColumnNumber: value.trimEnd().length + 1,
|
||||||
endColumnNumber: value.length,
|
endColumnNumber: value.length,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
|
||||||
const fix = (value: string) => value.trimEnd()
|
const fix = (value: string) => value.trimEnd()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,20 +2,25 @@ import { PathLintRule } from '../../types/LintRule'
|
|||||||
import { LintRuleType } from '../../types/LintRuleType'
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
import { Severity } from '../../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { LintConfig } from '../../types'
|
||||||
|
|
||||||
const name = 'lowerCaseFileNames'
|
const name = 'lowerCaseFileNames'
|
||||||
const description = 'Enforce the use of lower case file names.'
|
const description = 'Enforce the use of lower case file names.'
|
||||||
const message = 'File name contains uppercase characters'
|
const message = 'File name contains uppercase characters'
|
||||||
const test = (value: string) => {
|
|
||||||
|
const test = (value: string, config?: LintConfig) => {
|
||||||
|
const severity = config?.severityLevel[name] || Severity.Warning
|
||||||
const fileName = path.basename(value)
|
const fileName = path.basename(value)
|
||||||
|
|
||||||
if (fileName.toLocaleLowerCase() === fileName) return []
|
if (fileName.toLocaleLowerCase() === fileName) return []
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
message,
|
message,
|
||||||
lineNumber: 1,
|
lineNumber: 1,
|
||||||
startColumnNumber: 1,
|
startColumnNumber: 1,
|
||||||
endColumnNumber: 1,
|
endColumnNumber: 1,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ import { PathLintRule } from '../../types/LintRule'
|
|||||||
import { LintRuleType } from '../../types/LintRuleType'
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
import { Severity } from '../../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { LintConfig } from '../../types'
|
||||||
|
|
||||||
const name = 'noSpacesInFileNames'
|
const name = 'noSpacesInFileNames'
|
||||||
const description = 'Enforce the absence of spaces within file names.'
|
const description = 'Enforce the absence of spaces within file names.'
|
||||||
const message = 'File name contains spaces'
|
const message = 'File name contains spaces'
|
||||||
const test = (value: string) => {
|
|
||||||
|
const test = (value: string, config?: LintConfig) => {
|
||||||
|
const severity = config?.severityLevel[name] || Severity.Warning
|
||||||
const fileName = path.basename(value)
|
const fileName = path.basename(value)
|
||||||
|
|
||||||
if (fileName.includes(' ')) {
|
if (fileName.includes(' ')) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -15,7 +19,7 @@ const test = (value: string) => {
|
|||||||
lineNumber: 1,
|
lineNumber: 1,
|
||||||
startColumnNumber: 1,
|
startColumnNumber: 1,
|
||||||
endColumnNumber: 1,
|
endColumnNumber: 1,
|
||||||
severity: Severity.Warning
|
severity
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { LineEndings } from './LineEndings'
|
import { LineEndings } from './LineEndings'
|
||||||
import { LintConfig } from './LintConfig'
|
import { LintConfig } from './LintConfig'
|
||||||
import { LintRuleType } from './LintRuleType'
|
import { LintRuleType } from './LintRuleType'
|
||||||
|
import { Severity } from './Severity'
|
||||||
|
|
||||||
describe('LintConfig', () => {
|
describe('LintConfig', () => {
|
||||||
it('should create an empty instance', () => {
|
it('should create an empty instance', () => {
|
||||||
@@ -123,6 +124,23 @@ describe('LintConfig', () => {
|
|||||||
expect(config.lineEndings).toEqual(LineEndings.CRLF)
|
expect(config.lineEndings).toEqual(LineEndings.CRLF)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should create an instance with the severityLevel config', () => {
|
||||||
|
const config = new LintConfig({
|
||||||
|
severityLevel: {
|
||||||
|
hasDoxygenHeader: 'warn',
|
||||||
|
maxLineLength: 'error',
|
||||||
|
noTrailingSpaces: 'error'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.severityLevel).toEqual({
|
||||||
|
hasDoxygenHeader: Severity.Warning,
|
||||||
|
maxLineLength: Severity.Error,
|
||||||
|
noTrailingSpaces: Severity.Error
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('should create an instance with the line endings set to LF by default', () => {
|
it('should create an instance with the line endings set to LF by default', () => {
|
||||||
const config = new LintConfig({})
|
const config = new LintConfig({})
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { lowerCaseFileNames, noSpacesInFileNames } from '../rules/path'
|
|||||||
import { LineEndings } from './LineEndings'
|
import { LineEndings } from './LineEndings'
|
||||||
import { FileLintRule, LineLintRule, PathLintRule } from './LintRule'
|
import { FileLintRule, LineLintRule, PathLintRule } from './LintRule'
|
||||||
import { getDefaultHeader } from '../utils'
|
import { getDefaultHeader } from '../utils'
|
||||||
|
import { Severity } from './Severity'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LintConfig is the logical representation of the .sasjslint file.
|
* LintConfig is the logical representation of the .sasjslint file.
|
||||||
@@ -34,6 +35,7 @@ export class LintConfig {
|
|||||||
readonly indentationMultiple: number = 2
|
readonly indentationMultiple: number = 2
|
||||||
readonly lineEndings: LineEndings = LineEndings.LF
|
readonly lineEndings: LineEndings = LineEndings.LF
|
||||||
readonly defaultHeader: string = getDefaultHeader()
|
readonly defaultHeader: string = getDefaultHeader()
|
||||||
|
readonly severityLevel: { [key: string]: Severity } = {}
|
||||||
|
|
||||||
constructor(json?: any) {
|
constructor(json?: any) {
|
||||||
if (json?.ignoreList) {
|
if (json?.ignoreList) {
|
||||||
@@ -116,5 +118,12 @@ export class LintConfig {
|
|||||||
if (json?.strictMacroDefinition) {
|
if (json?.strictMacroDefinition) {
|
||||||
this.fileLintRules.push(strictMacroDefinition)
|
this.fileLintRules.push(strictMacroDefinition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (json?.severityLevel) {
|
||||||
|
for (const [rule, severity] of Object.entries(json.severityLevel)) {
|
||||||
|
if (severity === 'warn') this.severityLevel[rule] = Severity.Warning
|
||||||
|
if (severity === 'error') this.severityLevel[rule] = Severity.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,5 +36,5 @@ export interface FileLintRule extends LintRule {
|
|||||||
*/
|
*/
|
||||||
export interface PathLintRule extends LintRule {
|
export interface PathLintRule extends LintRule {
|
||||||
type: LintRuleType.Path
|
type: LintRuleType.Path
|
||||||
test: (value: string) => Diagnostic[]
|
test: (value: string, config?: LintConfig) => Diagnostic[]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user