mirror of
https://github.com/sasjs/lint.git
synced 2025-12-10 17:34:36 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb7f70e83a | ||
|
|
7f9ed5e61e | ||
|
|
63255fa3c8 | ||
|
|
00af205a55 | ||
|
|
e74663ba54 | ||
|
|
a9cb4d8dac | ||
|
|
ed58b288b5 | ||
|
|
be173d2e2b | ||
|
|
3e4809c352 | ||
|
|
f0ab349bf7 | ||
|
|
0c5588023d | ||
|
|
8badfd9358 | ||
| 0dfd1fb85b | |||
|
|
04cfa454f8 | ||
| 2cb73da0eb | |||
|
|
22cc42446c | ||
|
|
0fe79273e0 | ||
|
|
3d7f88aacb | ||
|
|
1677eca957 | ||
|
|
a1ebb51230 | ||
| 496e0bc8fc |
36
README.md
36
README.md
@@ -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.
|
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
|
# Linting
|
||||||
|
|
||||||
@sasjs/lint is used by the following products:
|
@sasjs/lint is used by the following products:
|
||||||
@@ -23,7 +25,7 @@ Configuration is via a `.sasjslint` file with the following structure (these are
|
|||||||
"hasDoxygenHeader": true,
|
"hasDoxygenHeader": true,
|
||||||
"hasMacroNameInMend": true,
|
"hasMacroNameInMend": true,
|
||||||
"hasMacroParentheses": true,
|
"hasMacroParentheses": true,
|
||||||
"ignoreList": ["sajsbuild/", "sasjsresults/"],
|
"ignoreList": ["sasjsbuild/", "sasjsresults/"],
|
||||||
"indentationMultiple": 2,
|
"indentationMultiple": 2,
|
||||||
"lineEndings": "off",
|
"lineEndings": "off",
|
||||||
"lowerCaseFileNames": true,
|
"lowerCaseFileNames": true,
|
||||||
@@ -49,9 +51,11 @@ Each setting can have three states:
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
Configuring a non-zero return code (ERROR) is helpful when running `sasjs lint` as part of a git pre-commit hook. An example is available [here](https://github.com/sasjs/template_jobs/blob/main/.git-hooks/pre-commit).
|
||||||
|
|
||||||
### allowedGremlins
|
### allowedGremlins
|
||||||
|
|
||||||
An array of hex codes that represents allowed gremlins (invisible / undesirable characters). To allow all gremlins, you can also set the `noGremlins` rule to `false`.
|
An array of hex codes that represents allowed gremlins (invisible / undesirable characters). To allow all gremlins, you can also set the `noGremlins` rule to `false`. The full gremlin list is [here](https://github.com/sasjs/lint/blob/main/src/utils/gremlinCharacters.ts).
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -228,6 +232,13 @@ In addition, when such files are used in URLs, they are often padded with a mess
|
|||||||
- Default: true
|
- Default: true
|
||||||
- Severity: WARNING
|
- Severity: WARNING
|
||||||
|
|
||||||
|
As an alternative (or in addition) to using a lint rule, you can also set the following in your `.gitignore` file to prevent files with spaces from being committed:
|
||||||
|
|
||||||
|
```
|
||||||
|
# prevent files/folders with spaces
|
||||||
|
**\ **
|
||||||
|
```
|
||||||
|
|
||||||
### noTabs
|
### noTabs
|
||||||
|
|
||||||
Whilst there are some arguments for using tabs (such as the ability to set your own indentation width, and to reduce character count) there are many, many, many developers who think otherwise. We're in that camp. Sorry (not sorry).
|
Whilst there are some arguments for using tabs (such as the ability to set your own indentation width, and to reduce character count) there are many, many, many developers who think otherwise. We're in that camp. Sorry (not sorry).
|
||||||
@@ -243,6 +254,21 @@ This will highlight lines with trailing spaces. Trailing spaces serve no useful
|
|||||||
- Default: true
|
- Default: true
|
||||||
- severity: WARNING
|
- 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
|
## 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:
|
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:
|
||||||
@@ -281,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
|
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
|
# 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?
|
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?
|
||||||
|
|||||||
@@ -20,9 +20,11 @@
|
|||||||
"noSpacesInFileNames": true,
|
"noSpacesInFileNames": true,
|
||||||
"noTabs": true,
|
"noTabs": true,
|
||||||
"noTrailingSpaces": true,
|
"noTrailingSpaces": true,
|
||||||
"lineEndings": "lf",
|
"lineEndings": "off",
|
||||||
"strictMacroDefinition": true,
|
"strictMacroDefinition": true,
|
||||||
"ignoreList": ["sajsbuild", "sasjsresults"]
|
"ignoreList": ["sajsbuild", "sasjsresults"],
|
||||||
|
"hasRequiredMacroOptions": false,
|
||||||
|
"requiredMacroOptions": []
|
||||||
},
|
},
|
||||||
"examples": [
|
"examples": [
|
||||||
{
|
{
|
||||||
@@ -43,7 +45,9 @@
|
|||||||
"hasMacroParentheses": true,
|
"hasMacroParentheses": true,
|
||||||
"lineEndings": "crlf",
|
"lineEndings": "crlf",
|
||||||
"strictMacroDefinition": true,
|
"strictMacroDefinition": true,
|
||||||
"ignoreList": ["sajsbuild", "sasjsresults"]
|
"ignoreList": ["sajsbuild", "sasjsresults"],
|
||||||
|
"hasRequiredMacroOptions": false,
|
||||||
|
"requiredMacroOptions": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -185,7 +189,7 @@
|
|||||||
"enum": ["lf", "crlf", "off"],
|
"enum": ["lf", "crlf", "off"],
|
||||||
"title": "lineEndings",
|
"title": "lineEndings",
|
||||||
"description": "Enforces the configured terminating character for each line. Shows a warning when incorrect line endings are present.",
|
"description": "Enforces the configured terminating character for each line. Shows a warning when incorrect line endings are present.",
|
||||||
"default": "lf",
|
"default": "off",
|
||||||
"examples": ["lf", "crlf"]
|
"examples": ["lf", "crlf"]
|
||||||
},
|
},
|
||||||
"strictMacroDefinition": {
|
"strictMacroDefinition": {
|
||||||
@@ -204,6 +208,22 @@
|
|||||||
"default": ["sasjsbuild/", "sasjsresults/"],
|
"default": ["sasjsbuild/", "sasjsresults/"],
|
||||||
"examples": ["sasjs/tests", "tmp/scratch.sas"]
|
"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": {
|
"severityLevel": {
|
||||||
"$id": "#/properties/severityLevel",
|
"$id": "#/properties/severityLevel",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -320,6 +340,13 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["error", "warn"],
|
"enum": ["error", "warn"],
|
||||||
"default": "warn"
|
"default": "warn"
|
||||||
|
},
|
||||||
|
"hasRequiredMacroOptions": {
|
||||||
|
"$id": "#/properties/severityLevel/hasRequiredMacroOptions",
|
||||||
|
"title": "hasRequiredMacroOptions",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["error", "warn"],
|
||||||
|
"default": "warn"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import { formatFolder } from './formatFolder'
|
|||||||
* @returns {Promise<FormatResult>} Resolves successfully when all SAS files in the current project have been formatted.
|
* @returns {Promise<FormatResult>} Resolves successfully when all SAS files in the current project have been formatted.
|
||||||
*/
|
*/
|
||||||
export const formatProject = async (): Promise<FormatResult> => {
|
export const formatProject = async (): Promise<FormatResult> => {
|
||||||
const projectRoot =
|
const projectRoot = (await getProjectRoot()) || process.currentDir
|
||||||
(await getProjectRoot()) || process.projectDir || process.currentDir
|
|
||||||
if (!projectRoot) {
|
if (!projectRoot) {
|
||||||
throw new Error('SASjs Project Root was not found.')
|
throw new Error('SASjs Project Root was not found.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.info(`Formatting all .sas files under ${projectRoot}`)
|
||||||
|
|
||||||
return await formatFolder(projectRoot)
|
return await formatFolder(projectRoot)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const lintFile = async (
|
|||||||
filePath: string,
|
filePath: string,
|
||||||
configuration?: LintConfig
|
configuration?: LintConfig
|
||||||
): Promise<Diagnostic[]> => {
|
): Promise<Diagnostic[]> => {
|
||||||
if (await isIgnored(filePath)) return []
|
if (await isIgnored(filePath, configuration)) return []
|
||||||
|
|
||||||
const config = configuration || (await getLintConfig())
|
const config = configuration || (await getLintConfig())
|
||||||
const text = await readFile(filePath)
|
const text = await readFile(filePath)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const lintFolder = async (
|
|||||||
const config = configuration || (await getLintConfig())
|
const config = configuration || (await getLintConfig())
|
||||||
let diagnostics: Map<string, Diagnostic[]> = new Map<string, Diagnostic[]>()
|
let diagnostics: Map<string, Diagnostic[]> = new Map<string, Diagnostic[]>()
|
||||||
|
|
||||||
if (await isIgnored(folderPath)) return diagnostics
|
if (await isIgnored(folderPath, config)) return diagnostics
|
||||||
|
|
||||||
const fileNames = await listSasFiles(folderPath)
|
const fileNames = await listSasFiles(folderPath)
|
||||||
await asyncForEach(fileNames, async (fileName) => {
|
await asyncForEach(fileNames, async (fileName) => {
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import { lintFolder } from './lintFolder'
|
|||||||
* @returns {Promise<Map<string, Diagnostic[]>>} Resolves with a map with array of diagnostic objects, each containing a warning, line number and column number, and grouped by file path.
|
* @returns {Promise<Map<string, Diagnostic[]>>} Resolves with a map with array of diagnostic objects, each containing a warning, line number and column number, and grouped by file path.
|
||||||
*/
|
*/
|
||||||
export const lintProject = async () => {
|
export const lintProject = async () => {
|
||||||
const projectRoot =
|
const projectRoot = (await getProjectRoot()) || process.currentDir
|
||||||
(await getProjectRoot()) || process.projectDir || process.currentDir
|
|
||||||
if (!projectRoot) {
|
if (!projectRoot) {
|
||||||
throw new Error('SASjs Project Root was not found.')
|
throw new Error('SASjs Project Root was not found.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.info(`Linting all .sas files under ${projectRoot}`)
|
||||||
|
|
||||||
return await lintFolder(projectRoot)
|
return await lintFolder(projectRoot)
|
||||||
}
|
}
|
||||||
|
|||||||
123
src/rules/file/hasRequiredMacroOptions.spec.ts
Normal file
123
src/rules/file/hasRequiredMacroOptions.spec.ts
Normal 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
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
52
src/rules/file/hasRequiredMacroOptions.ts
Normal file
52
src/rules/file/hasRequiredMacroOptions.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -4,3 +4,4 @@ export { hasMacroParentheses } from './hasMacroParentheses'
|
|||||||
export { lineEndings } from './lineEndings'
|
export { lineEndings } from './lineEndings'
|
||||||
export { noNestedMacros } from './noNestedMacros'
|
export { noNestedMacros } from './noNestedMacros'
|
||||||
export { strictMacroDefinition } from './strictMacroDefinition'
|
export { strictMacroDefinition } from './strictMacroDefinition'
|
||||||
|
export { hasRequiredMacroOptions } from './hasRequiredMacroOptions'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { hasRequiredMacroOptions } from '../rules/file'
|
||||||
import { LineEndings } from './LineEndings'
|
import { LineEndings } from './LineEndings'
|
||||||
import { LintConfig } from './LintConfig'
|
import { LintConfig } from './LintConfig'
|
||||||
import { LintRuleType } from './LintRuleType'
|
import { LintRuleType } from './LintRuleType'
|
||||||
@@ -168,6 +169,7 @@ describe('LintConfig', () => {
|
|||||||
hasMacroNameInMend: true,
|
hasMacroNameInMend: true,
|
||||||
noNestedMacros: true,
|
noNestedMacros: true,
|
||||||
hasMacroParentheses: true,
|
hasMacroParentheses: true,
|
||||||
|
hasRequiredMacroOptions: true,
|
||||||
noGremlins: true,
|
noGremlins: true,
|
||||||
lineEndings: 'lf'
|
lineEndings: 'lf'
|
||||||
})
|
})
|
||||||
@@ -187,7 +189,7 @@ describe('LintConfig', () => {
|
|||||||
expect(config.lineLintRules[5].name).toEqual('noGremlins')
|
expect(config.lineLintRules[5].name).toEqual('noGremlins')
|
||||||
expect(config.lineLintRules[5].type).toEqual(LintRuleType.Line)
|
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].name).toEqual('lineEndings')
|
||||||
expect(config.fileLintRules[0].type).toEqual(LintRuleType.File)
|
expect(config.fileLintRules[0].type).toEqual(LintRuleType.File)
|
||||||
expect(config.fileLintRules[1].name).toEqual('hasDoxygenHeader')
|
expect(config.fileLintRules[1].name).toEqual('hasDoxygenHeader')
|
||||||
@@ -200,6 +202,8 @@ describe('LintConfig', () => {
|
|||||||
expect(config.fileLintRules[4].type).toEqual(LintRuleType.File)
|
expect(config.fileLintRules[4].type).toEqual(LintRuleType.File)
|
||||||
expect(config.fileLintRules[5].name).toEqual('strictMacroDefinition')
|
expect(config.fileLintRules[5].name).toEqual('strictMacroDefinition')
|
||||||
expect(config.fileLintRules[5].type).toEqual(LintRuleType.File)
|
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.length).toEqual(2)
|
||||||
expect(config.pathLintRules[0].name).toEqual('noSpacesInFileNames')
|
expect(config.pathLintRules[0].name).toEqual('noSpacesInFileNames')
|
||||||
@@ -207,4 +211,25 @@ describe('LintConfig', () => {
|
|||||||
expect(config.pathLintRules[1].name).toEqual('lowerCaseFileNames')
|
expect(config.pathLintRules[1].name).toEqual('lowerCaseFileNames')
|
||||||
expect(config.pathLintRules[1].type).toEqual(LintRuleType.Path)
|
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.`
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import {
|
|||||||
noNestedMacros,
|
noNestedMacros,
|
||||||
hasMacroParentheses,
|
hasMacroParentheses,
|
||||||
lineEndings,
|
lineEndings,
|
||||||
strictMacroDefinition
|
strictMacroDefinition,
|
||||||
|
hasRequiredMacroOptions
|
||||||
} from '../rules/file'
|
} from '../rules/file'
|
||||||
import {
|
import {
|
||||||
indentationMultiple,
|
indentationMultiple,
|
||||||
@@ -40,6 +41,7 @@ export class LintConfig {
|
|||||||
readonly lineEndings: LineEndings = LineEndings.LF
|
readonly lineEndings: LineEndings = LineEndings.LF
|
||||||
readonly defaultHeader: string = getDefaultHeader()
|
readonly defaultHeader: string = getDefaultHeader()
|
||||||
readonly severityLevel: { [key: string]: Severity } = {}
|
readonly severityLevel: { [key: string]: Severity } = {}
|
||||||
|
readonly requiredMacroOptions: string[] = []
|
||||||
|
|
||||||
constructor(json?: any) {
|
constructor(json?: any) {
|
||||||
if (json?.ignoreList) {
|
if (json?.ignoreList) {
|
||||||
@@ -132,6 +134,31 @@ export class LintConfig {
|
|||||||
this.fileLintRules.push(strictMacroDefinition)
|
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) {
|
if (json?.noGremlins !== false) {
|
||||||
this.lineLintRules.push(noGremlins)
|
this.lineLintRules.push(noGremlins)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import os from 'os'
|
||||||
import { LintConfig } from '../types/LintConfig'
|
import { LintConfig } from '../types/LintConfig'
|
||||||
import { readFile } from '@sasjs/utils/file'
|
import { readFile } from '@sasjs/utils/file'
|
||||||
import { getProjectRoot } from './getProjectRoot'
|
import { getProjectRoot } from './getProjectRoot'
|
||||||
|
import { LineEndings } from '../types/LineEndings'
|
||||||
|
|
||||||
export const getDefaultHeader = () =>
|
export const getDefaultHeader = () =>
|
||||||
`/**{lineEnding} @file{lineEnding} @brief <Your brief here>{lineEnding} <h4> SAS Macros </h4>{lineEnding}**/`
|
`/**{lineEnding} @file{lineEnding} @brief <Your brief here>{lineEnding} <h4> SAS Macros </h4>{lineEnding}**/`
|
||||||
@@ -10,6 +12,7 @@ export const getDefaultHeader = () =>
|
|||||||
* Default configuration that is used when a .sasjslint file is not found
|
* Default configuration that is used when a .sasjslint file is not found
|
||||||
*/
|
*/
|
||||||
export const DefaultLintConfiguration = {
|
export const DefaultLintConfiguration = {
|
||||||
|
lineEndings: LineEndings.OFF,
|
||||||
noTrailingSpaces: true,
|
noTrailingSpaces: true,
|
||||||
noEncodedPasswords: true,
|
noEncodedPasswords: true,
|
||||||
hasDoxygenHeader: true,
|
hasDoxygenHeader: true,
|
||||||
@@ -29,14 +32,15 @@ export const DefaultLintConfiguration = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the config from the .sasjslint file and creates a LintConfig object.
|
* Fetches the config from the .sasjslint file (at project root or home directory) and creates a LintConfig object.
|
||||||
* Returns the default configuration when a .sasjslint file is unavailable.
|
* Returns the default configuration when a .sasjslint file is unavailable.
|
||||||
* @returns {Promise<LintConfig>} resolves with an object representing the current lint configuration.
|
* @returns {Promise<LintConfig>} resolves with an object representing the current lint configuration.
|
||||||
*/
|
*/
|
||||||
export async function getLintConfig(): Promise<LintConfig> {
|
export async function getLintConfig(): Promise<LintConfig> {
|
||||||
const projectRoot = await getProjectRoot()
|
const projectRoot = await getProjectRoot()
|
||||||
|
const lintFileLocation = projectRoot || os.homedir()
|
||||||
const configuration = await readFile(
|
const configuration = await readFile(
|
||||||
path.join(projectRoot, '.sasjslint')
|
path.join(lintFileLocation, '.sasjslint')
|
||||||
).catch((_) => {
|
).catch((_) => {
|
||||||
return JSON.stringify(DefaultLintConfiguration)
|
return JSON.stringify(DefaultLintConfiguration)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import os from 'os'
|
||||||
import { fileExists } from '@sasjs/utils/file'
|
import { fileExists } from '@sasjs/utils/file'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,10 +12,11 @@ export async function getProjectRoot(): Promise<string> {
|
|||||||
let rootFound = false
|
let rootFound = false
|
||||||
let i = 1
|
let i = 1
|
||||||
let currentLocation = process.cwd()
|
let currentLocation = process.cwd()
|
||||||
|
const homeDir = os.homedir()
|
||||||
|
|
||||||
const maxLevels = currentLocation.split(path.sep).length
|
const maxLevels = currentLocation.split(path.sep).length
|
||||||
|
|
||||||
while (i <= maxLevels && !rootFound) {
|
while (i <= maxLevels && !rootFound && currentLocation !== homeDir) {
|
||||||
const isRoot = await fileExists(path.join(currentLocation, '.sasjslint'))
|
const isRoot = await fileExists(path.join(currentLocation, '.sasjslint'))
|
||||||
|
|
||||||
if (isRoot) {
|
if (isRoot) {
|
||||||
|
|||||||
Reference in New Issue
Block a user