mirror of
https://github.com/sasjs/lint.git
synced 2025-12-10 17:34:36 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9984a373df | ||
| 0c79a1ef85 | |||
|
|
0bd57489b7 | ||
|
|
f59fd4c3f3 | ||
| 5245246818 | |||
| 636703b326 | |||
| 24fba7867c | |||
| 5c44ec400d | |||
| c0fdfc6ac9 | |||
| 4b16e0c52a | |||
| 8cf4f34e30 | |||
| 97e3490a8d | |||
|
|
f6ddfa833d | ||
|
|
e227f16f88 | ||
|
|
7de907057d | ||
| 80c90ebda1 | |||
| c5ead229a9 | |||
| 7d6fc8eb8c | |||
|
|
65772804fe | ||
|
|
48a6628ec5 |
11
README.md
11
README.md
@@ -1,4 +1,3 @@
|
||||
[](/LICENSE)
|
||||

|
||||
[](https://github.com/sasjs/lint/issues?q=is%3Aissue+is%3Aclosed)
|
||||
[](https://github.com/sasjs/lint/issues)
|
||||
@@ -29,6 +28,7 @@ Configuration is via a `.sasjslint` file with the following structure (these are
|
||||
"lowerCaseFileNames": true,
|
||||
"maxLineLength": 80,
|
||||
"noNestedMacros": true,
|
||||
"noGremlins": true,
|
||||
"noSpacesInFileNames": true,
|
||||
"noTabs": true,
|
||||
"noTrailingSpaces": true,
|
||||
@@ -125,6 +125,15 @@ We strongly recommend a line length limit, and set the bar at 80. To turn this f
|
||||
- Default: 80
|
||||
- Severity: WARNING
|
||||
|
||||
### noGremlins
|
||||
|
||||
Capture zero-width whitespace and other non-standard characters. The logic is borrowed from the [VSCode Gremlins Extension](https://github.com/nhoizey/vscode-gremlins) - if you are looking for more advanced gremlin zapping capabilities, we highly recommend to use their extension instead.
|
||||
|
||||
The list of characters can be found in this file: [https://github.com/sasjs/lint/blob/main/src/utils/gremlinCharacters.ts](https://github.com/sasjs/lint/blob/main/src/utils/gremlinCharacters.ts)
|
||||
|
||||
- Default: true
|
||||
- Severity: WARNING
|
||||
|
||||
### noNestedMacros
|
||||
|
||||
Where macros are defined inside other macros, they are recompiled every time the outer macro is invoked. Hence, it is widely considered inefficient, and bad practice, to nest macro definitions.
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"indentationMultiple": 2,
|
||||
"lowerCaseFileNames": true,
|
||||
"maxLineLength": 80,
|
||||
"noGremlins": true,
|
||||
"noNestedMacros": true,
|
||||
"noSpacesInFileNames": true,
|
||||
"noTabs": true,
|
||||
@@ -29,6 +30,7 @@
|
||||
"noSpacesInFileNames": true,
|
||||
"lowerCaseFileNames": true,
|
||||
"maxLineLength": 80,
|
||||
"noGremlins": true,
|
||||
"noTabs": true,
|
||||
"indentationMultiple": 4,
|
||||
"hasMacroNameInMend": true,
|
||||
@@ -64,6 +66,14 @@
|
||||
"default": "/**{lineEnding} @file{lineEnding} @brief <Your brief here>{lineEnding} <h4> SAS Macros </h4>{lineEnding}**/",
|
||||
"examples": []
|
||||
},
|
||||
"noGremlins": {
|
||||
"$id": "#/properties/noGremlins",
|
||||
"type": "boolean",
|
||||
"title": "noGremlins",
|
||||
"description": "Captures problematic characters such as zero-width whitespace and others that look valid but usually are not (such as the en dash)",
|
||||
"default": [true],
|
||||
"examples": [true, false]
|
||||
},
|
||||
"hasMacroNameInMend": {
|
||||
"$id": "#/properties/hasMacroNameInMend",
|
||||
"type": "boolean",
|
||||
@@ -193,6 +203,13 @@
|
||||
"enum": ["error", "warn"],
|
||||
"default": "warn"
|
||||
},
|
||||
"noGremlins": {
|
||||
"$id": "#/properties/severityLevel/noGremlins",
|
||||
"title": "noGremlins",
|
||||
"type": "string",
|
||||
"enum": ["error", "warn"],
|
||||
"default": "warn"
|
||||
},
|
||||
"hasMacroNameInMend": {
|
||||
"$id": "#/properties/severityLevel/hasMacroNameInMend",
|
||||
"title": "hasMacroNameInMend",
|
||||
|
||||
@@ -32,16 +32,8 @@ describe('formatFile', () => {
|
||||
const expectedContent = `/**\r\n @file\r\n @brief <Your brief here>\r\n <h4> SAS Macros </h4>\r\n**/\r\n%macro somemacro();\r\n%put 'hello';\r\n%mend;`
|
||||
const expectedResult = {
|
||||
updatedFilePaths: [path.join(__dirname, 'format-file-config.sas')],
|
||||
fixedDiagnosticsCount: 2,
|
||||
unfixedDiagnostics: [
|
||||
{
|
||||
endColumnNumber: 7,
|
||||
lineNumber: 8,
|
||||
message: '%mend statement is missing macro name - somemacro',
|
||||
severity: 1,
|
||||
startColumnNumber: 1
|
||||
}
|
||||
]
|
||||
fixedDiagnosticsCount: 4,
|
||||
unfixedDiagnostics: []
|
||||
}
|
||||
await createFile(path.join(__dirname, 'format-file-config.sas'), content)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export const formatFile = async (
|
||||
configuration?: LintConfig
|
||||
): Promise<FormatResult> => {
|
||||
const config = configuration || (await getLintConfig())
|
||||
const diagnosticsBeforeFormat = await lintFile(filePath)
|
||||
const diagnosticsBeforeFormat = await lintFile(filePath, config)
|
||||
const diagnosticsCountBeforeFormat = diagnosticsBeforeFormat.length
|
||||
|
||||
const text = await readFile(filePath)
|
||||
@@ -25,7 +25,7 @@ export const formatFile = async (
|
||||
|
||||
await createFile(filePath, formattedText)
|
||||
|
||||
const diagnosticsAfterFormat = await lintFile(filePath)
|
||||
const diagnosticsAfterFormat = await lintFile(filePath, config)
|
||||
const diagnosticsCountAfterFormat = diagnosticsAfterFormat.length
|
||||
|
||||
const fixedDiagnosticsCount =
|
||||
|
||||
@@ -30,7 +30,7 @@ export const processLine = (config: LintConfig, line: string): string => {
|
||||
config.lineLintRules
|
||||
.filter((r) => !!r.fix)
|
||||
.forEach((rule) => {
|
||||
processedLine = rule.fix!(line, config)
|
||||
processedLine = rule.fix!(processedLine, config)
|
||||
})
|
||||
|
||||
return processedLine
|
||||
|
||||
@@ -46,7 +46,7 @@ const expectedDiagnostics = [
|
||||
severity: Severity.Error
|
||||
},
|
||||
{
|
||||
message: 'Line contains tab indentation',
|
||||
message: 'Line contains a tab character (09x)',
|
||||
lineNumber: 7,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
|
||||
@@ -53,7 +53,7 @@ const expectedDiagnostics = [
|
||||
severity: Severity.Error
|
||||
},
|
||||
{
|
||||
message: 'Line contains tab indentation',
|
||||
message: 'Line contains a tab character (09x)',
|
||||
lineNumber: 7,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
|
||||
@@ -51,7 +51,7 @@ const expectedDiagnostics = [
|
||||
severity: Severity.Error
|
||||
},
|
||||
{
|
||||
message: 'Line contains tab indentation',
|
||||
message: 'Line contains a tab character (09x)',
|
||||
lineNumber: 7,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
|
||||
@@ -110,7 +110,7 @@ const processOptions = (
|
||||
const severity = config?.severityLevel[name] || Severity.Warning
|
||||
|
||||
if (optionsPresent) {
|
||||
const regex = new RegExp(/="(.*?)"/, 'g')
|
||||
const regex = new RegExp(/=["|'](.*?)["|']/, 'g')
|
||||
|
||||
let result = regex.exec(optionsPresent)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { noGremlins } from './noGremlins'
|
||||
export { indentationMultiple } from './indentationMultiple'
|
||||
export { maxLineLength } from './maxLineLength'
|
||||
export { noEncodedPasswords } from './noEncodedPasswords'
|
||||
|
||||
15
src/rules/line/noGremlins.spec.ts
Normal file
15
src/rules/line/noGremlins.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Severity } from '../../types/Severity'
|
||||
import { noGremlins } from './noGremlins'
|
||||
|
||||
describe('noTabs', () => {
|
||||
it('should return an empty array when the line does not have any gremlin', () => {
|
||||
const line = "%put 'hello';"
|
||||
expect(noGremlins.test(line, 1)).toEqual([])
|
||||
})
|
||||
|
||||
it('should return a diagnostic array when the line contains gremlins', () => {
|
||||
const line = "– ‘ %put 'hello';"
|
||||
const diagnostics = noGremlins.test(line, 1)
|
||||
expect(diagnostics.length).toEqual(2)
|
||||
})
|
||||
})
|
||||
59
src/rules/line/noGremlins.ts
Normal file
59
src/rules/line/noGremlins.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Diagnostic, LintConfig } from '../../types'
|
||||
import { LineLintRule } from '../../types/LintRule'
|
||||
import { LintRuleType } from '../../types/LintRuleType'
|
||||
import { Severity } from '../../types/Severity'
|
||||
import { gremlinCharacters } from '../../utils'
|
||||
|
||||
const name = 'noGremlins'
|
||||
const description = 'Disallow characters specified in gremlins array'
|
||||
const message = 'Line contains a gremlin'
|
||||
|
||||
const test = (value: string, lineNumber: number, config?: LintConfig) => {
|
||||
const severity = config?.severityLevel[name] || Severity.Warning
|
||||
|
||||
const diagnostics: Diagnostic[] = []
|
||||
|
||||
const gremlins: any = {}
|
||||
|
||||
for (const [hexCode, config] of Object.entries(gremlinCharacters)) {
|
||||
gremlins[charFromHex(hexCode)] = Object.assign({}, config, {
|
||||
hexCode
|
||||
})
|
||||
}
|
||||
|
||||
const regexpWithAllChars = new RegExp(
|
||||
Object.keys(gremlins)
|
||||
.map((char) => `${char}+`)
|
||||
.join('|'),
|
||||
'g'
|
||||
)
|
||||
|
||||
let match
|
||||
while ((match = regexpWithAllChars.exec(value))) {
|
||||
const matchedCharacter = match[0][0]
|
||||
const gremlin = gremlins[matchedCharacter]
|
||||
|
||||
diagnostics.push({
|
||||
message: `${message}: ${gremlin.description}, hexCode(${gremlin.hexCode})`,
|
||||
lineNumber,
|
||||
startColumnNumber: match.index + 1,
|
||||
endColumnNumber: match.index + 1 + match[0].length,
|
||||
severity
|
||||
})
|
||||
}
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
/**
|
||||
* Lint rule that checks if a given line of text contains any gremlins.
|
||||
*/
|
||||
export const noGremlins: LineLintRule = {
|
||||
type: LintRuleType.Line,
|
||||
name,
|
||||
description,
|
||||
message,
|
||||
test
|
||||
}
|
||||
|
||||
const charFromHex = (hexCode: string) => String.fromCodePoint(parseInt(hexCode))
|
||||
@@ -11,7 +11,7 @@ describe('noTabs', () => {
|
||||
const line = "\t%put 'hello';"
|
||||
expect(noTabs.test(line, 1)).toEqual([
|
||||
{
|
||||
message: 'Line contains tab indentation',
|
||||
message: 'Line contains a tab character (09x)',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getIndicesOf } from '../../utils'
|
||||
const name = 'noTabs'
|
||||
const alias = 'noTabIndentation'
|
||||
const description = 'Disallow indenting with tabs.'
|
||||
const message = 'Line contains tab indentation'
|
||||
const message = 'Line contains a tab character (09x)'
|
||||
|
||||
const test = (value: string, lineNumber: number, config?: LintConfig) => {
|
||||
const severity =
|
||||
|
||||
@@ -4,96 +4,75 @@ import { LintRuleType } from './LintRuleType'
|
||||
import { Severity } from './Severity'
|
||||
|
||||
describe('LintConfig', () => {
|
||||
it('should create an empty instance', () => {
|
||||
it('should create an instance with default values when no configuration is provided', () => {
|
||||
const config = new LintConfig()
|
||||
|
||||
expect(config).toBeTruthy()
|
||||
expect(config.fileLintRules.length).toEqual(0)
|
||||
expect(config.lineLintRules.length).toEqual(0)
|
||||
})
|
||||
|
||||
it('should create an instance with the noTrailingSpaces flag set', () => {
|
||||
const config = new LintConfig({ noTrailingSpaces: true })
|
||||
it('should create an instance with the noTrailingSpaces flag off', () => {
|
||||
const config = new LintConfig({ noTrailingSpaces: false })
|
||||
|
||||
expect(config).toBeTruthy()
|
||||
expect(config.lineLintRules.length).toEqual(1)
|
||||
expect(config.lineLintRules[0].name).toEqual('noTrailingSpaces')
|
||||
expect(config.lineLintRules[0].type).toEqual(LintRuleType.Line)
|
||||
expect(config.fileLintRules.length).toEqual(0)
|
||||
expect(config.lineLintRules.length).toBeGreaterThan(0)
|
||||
expect(config.fileLintRules.length).toBeGreaterThan(0)
|
||||
expect(
|
||||
config.lineLintRules.find((rule) => rule.name === 'noTrailingSpaces')
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should create an instance with the noEncodedPasswords flag set', () => {
|
||||
const config = new LintConfig({ noEncodedPasswords: true })
|
||||
it('should create an instance with the noEncodedPasswords flag off', () => {
|
||||
const config = new LintConfig({ noEncodedPasswords: false })
|
||||
|
||||
expect(config).toBeTruthy()
|
||||
expect(config.lineLintRules.length).toEqual(1)
|
||||
expect(config.lineLintRules[0].name).toEqual('noEncodedPasswords')
|
||||
expect(config.lineLintRules[0].type).toEqual(LintRuleType.Line)
|
||||
expect(config.fileLintRules.length).toEqual(0)
|
||||
expect(config.lineLintRules.length).toBeGreaterThan(0)
|
||||
expect(config.fileLintRules.length).toBeGreaterThan(0)
|
||||
expect(
|
||||
config.lineLintRules.find((rule) => rule.name === 'noEncodedPasswords')
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should create an instance with the hasDoxygenHeader flag set', () => {
|
||||
const config = new LintConfig({ hasDoxygenHeader: true })
|
||||
it('should create an instance with the hasDoxygenHeader flag off', () => {
|
||||
const config = new LintConfig({ hasDoxygenHeader: false })
|
||||
|
||||
expect(config).toBeTruthy()
|
||||
expect(config.lineLintRules.length).toEqual(0)
|
||||
expect(config.fileLintRules.length).toEqual(1)
|
||||
expect(config.fileLintRules[0].name).toEqual('hasDoxygenHeader')
|
||||
expect(config.fileLintRules[0].type).toEqual(LintRuleType.File)
|
||||
})
|
||||
|
||||
it('should create an instance with the hasMacroNameInMend flag set', () => {
|
||||
const config = new LintConfig({ hasMacroNameInMend: true })
|
||||
|
||||
expect(config).toBeTruthy()
|
||||
expect(config.lineLintRules.length).toEqual(0)
|
||||
expect(config.fileLintRules.length).toEqual(1)
|
||||
expect(config.fileLintRules[0].name).toEqual('hasMacroNameInMend')
|
||||
expect(config.fileLintRules[0].type).toEqual(LintRuleType.File)
|
||||
expect(config.lineLintRules.length).toBeGreaterThan(0)
|
||||
expect(config.fileLintRules.length).toBeGreaterThan(0)
|
||||
expect(
|
||||
config.fileLintRules.find((rule) => rule.name === 'hasDoxygenHeader')
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should create an instance with the hasMacroNameInMend flag off', () => {
|
||||
const config = new LintConfig({ hasMacroNameInMend: false })
|
||||
|
||||
expect(config).toBeTruthy()
|
||||
expect(config.lineLintRules.length).toEqual(0)
|
||||
expect(config.fileLintRules.length).toEqual(0)
|
||||
})
|
||||
|
||||
it('should create an instance with the noNestedMacros flag set', () => {
|
||||
const config = new LintConfig({ noNestedMacros: true })
|
||||
|
||||
expect(config).toBeTruthy()
|
||||
expect(config.lineLintRules.length).toEqual(0)
|
||||
expect(config.fileLintRules.length).toEqual(1)
|
||||
expect(config.fileLintRules[0].name).toEqual('noNestedMacros')
|
||||
expect(config.fileLintRules[0].type).toEqual(LintRuleType.File)
|
||||
expect(config.lineLintRules.length).toBeGreaterThan(0)
|
||||
expect(config.fileLintRules.length).toBeGreaterThan(0)
|
||||
expect(
|
||||
config.fileLintRules.find((rule) => rule.name === 'hasMacroNameInMend')
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should create an instance with the noNestedMacros flag off', () => {
|
||||
const config = new LintConfig({ noNestedMacros: false })
|
||||
|
||||
expect(config).toBeTruthy()
|
||||
expect(config.lineLintRules.length).toEqual(0)
|
||||
expect(config.fileLintRules.length).toEqual(0)
|
||||
})
|
||||
|
||||
it('should create an instance with the hasMacroParentheses flag set', () => {
|
||||
const config = new LintConfig({ hasMacroParentheses: true })
|
||||
|
||||
expect(config).toBeTruthy()
|
||||
expect(config.lineLintRules.length).toEqual(0)
|
||||
expect(config.fileLintRules.length).toEqual(1)
|
||||
expect(config.fileLintRules[0].name).toEqual('hasMacroParentheses')
|
||||
expect(config.fileLintRules[0].type).toEqual(LintRuleType.File)
|
||||
expect(config.lineLintRules.length).toBeGreaterThan(0)
|
||||
expect(config.fileLintRules.length).toBeGreaterThan(0)
|
||||
expect(
|
||||
config.fileLintRules.find((rule) => rule.name === 'noNestedMacros')
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should create an instance with the hasMacroParentheses flag off', () => {
|
||||
const config = new LintConfig({ hasMacroParentheses: false })
|
||||
|
||||
expect(config).toBeTruthy()
|
||||
expect(config.lineLintRules.length).toEqual(0)
|
||||
expect(config.fileLintRules.length).toEqual(0)
|
||||
expect(config.lineLintRules.length).toBeGreaterThan(0)
|
||||
expect(config.fileLintRules.length).toBeGreaterThan(0)
|
||||
expect(
|
||||
config.fileLintRules.find((rule) => rule.name === 'hasMacroParentheses')
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should create an instance with the indentation multiple set', () => {
|
||||
@@ -166,11 +145,12 @@ describe('LintConfig', () => {
|
||||
indentationMultiple: 2,
|
||||
hasMacroNameInMend: true,
|
||||
noNestedMacros: true,
|
||||
hasMacroParentheses: true
|
||||
hasMacroParentheses: true,
|
||||
noGremlins: true
|
||||
})
|
||||
|
||||
expect(config).toBeTruthy()
|
||||
expect(config.lineLintRules.length).toEqual(5)
|
||||
expect(config.lineLintRules.length).toEqual(6)
|
||||
expect(config.lineLintRules[0].name).toEqual('noTrailingSpaces')
|
||||
expect(config.lineLintRules[0].type).toEqual(LintRuleType.Line)
|
||||
expect(config.lineLintRules[1].name).toEqual('noEncodedPasswords')
|
||||
@@ -181,16 +161,22 @@ describe('LintConfig', () => {
|
||||
expect(config.lineLintRules[3].type).toEqual(LintRuleType.Line)
|
||||
expect(config.lineLintRules[4].name).toEqual('indentationMultiple')
|
||||
expect(config.lineLintRules[4].type).toEqual(LintRuleType.Line)
|
||||
expect(config.lineLintRules[5].name).toEqual('noGremlins')
|
||||
expect(config.lineLintRules[5].type).toEqual(LintRuleType.Line)
|
||||
|
||||
expect(config.fileLintRules.length).toEqual(4)
|
||||
expect(config.fileLintRules[0].name).toEqual('hasDoxygenHeader')
|
||||
expect(config.fileLintRules.length).toEqual(6)
|
||||
expect(config.fileLintRules[0].name).toEqual('lineEndings')
|
||||
expect(config.fileLintRules[0].type).toEqual(LintRuleType.File)
|
||||
expect(config.fileLintRules[1].name).toEqual('hasMacroNameInMend')
|
||||
expect(config.fileLintRules[1].name).toEqual('hasDoxygenHeader')
|
||||
expect(config.fileLintRules[1].type).toEqual(LintRuleType.File)
|
||||
expect(config.fileLintRules[2].name).toEqual('noNestedMacros')
|
||||
expect(config.fileLintRules[2].name).toEqual('hasMacroNameInMend')
|
||||
expect(config.fileLintRules[2].type).toEqual(LintRuleType.File)
|
||||
expect(config.fileLintRules[3].name).toEqual('hasMacroParentheses')
|
||||
expect(config.fileLintRules[3].name).toEqual('noNestedMacros')
|
||||
expect(config.fileLintRules[3].type).toEqual(LintRuleType.File)
|
||||
expect(config.fileLintRules[4].name).toEqual('hasMacroParentheses')
|
||||
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.pathLintRules.length).toEqual(2)
|
||||
expect(config.pathLintRules[0].name).toEqual('noSpacesInFileNames')
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
maxLineLength,
|
||||
noEncodedPasswords,
|
||||
noTabs,
|
||||
noTrailingSpaces
|
||||
noTrailingSpaces,
|
||||
noGremlins
|
||||
} from '../rules/line'
|
||||
import { lowerCaseFileNames, noSpacesInFileNames } from '../rules/path'
|
||||
import { LineEndings } from './LineEndings'
|
||||
@@ -52,23 +53,25 @@ export class LintConfig {
|
||||
}
|
||||
}
|
||||
|
||||
if (json?.noTrailingSpaces) {
|
||||
if (json?.noTrailingSpaces !== false) {
|
||||
this.lineLintRules.push(noTrailingSpaces)
|
||||
}
|
||||
|
||||
if (json?.noEncodedPasswords) {
|
||||
if (json?.noEncodedPasswords !== false) {
|
||||
this.lineLintRules.push(noEncodedPasswords)
|
||||
}
|
||||
|
||||
if (json?.noTabs || json?.noTabIndentation) {
|
||||
this.lineLintRules.push(noTabs)
|
||||
this.lineLintRules.push(noTabs)
|
||||
if (json?.noTabs === false || json?.noTabIndentation === false) {
|
||||
this.lineLintRules.pop()
|
||||
}
|
||||
|
||||
if (json?.maxLineLength) {
|
||||
this.lineLintRules.push(maxLineLength)
|
||||
if (!isNaN(json?.maxLineLength)) {
|
||||
this.maxLineLength = json.maxLineLength
|
||||
this.lineLintRules.push(maxLineLength)
|
||||
}
|
||||
|
||||
this.fileLintRules.push(lineEndings)
|
||||
if (json?.lineEndings) {
|
||||
if (
|
||||
json.lineEndings !== LineEndings.LF &&
|
||||
@@ -79,15 +82,14 @@ export class LintConfig {
|
||||
)
|
||||
}
|
||||
this.lineEndings = json.lineEndings
|
||||
this.fileLintRules.push(lineEndings)
|
||||
}
|
||||
|
||||
this.lineLintRules.push(indentationMultiple)
|
||||
if (!isNaN(json?.indentationMultiple)) {
|
||||
this.indentationMultiple = json.indentationMultiple as number
|
||||
this.lineLintRules.push(indentationMultiple)
|
||||
}
|
||||
|
||||
if (json?.hasDoxygenHeader) {
|
||||
if (json?.hasDoxygenHeader !== false) {
|
||||
this.fileLintRules.push(hasDoxygenHeader)
|
||||
}
|
||||
|
||||
@@ -95,11 +97,11 @@ export class LintConfig {
|
||||
this.defaultHeader = json.defaultHeader
|
||||
}
|
||||
|
||||
if (json?.noSpacesInFileNames) {
|
||||
if (json?.noSpacesInFileNames !== false) {
|
||||
this.pathLintRules.push(noSpacesInFileNames)
|
||||
}
|
||||
|
||||
if (json?.lowerCaseFileNames) {
|
||||
if (json?.lowerCaseFileNames !== false) {
|
||||
this.pathLintRules.push(lowerCaseFileNames)
|
||||
}
|
||||
|
||||
@@ -107,18 +109,22 @@ export class LintConfig {
|
||||
this.fileLintRules.push(hasMacroNameInMend)
|
||||
}
|
||||
|
||||
if (json?.noNestedMacros) {
|
||||
if (json?.noNestedMacros !== false) {
|
||||
this.fileLintRules.push(noNestedMacros)
|
||||
}
|
||||
|
||||
if (json?.hasMacroParentheses) {
|
||||
if (json?.hasMacroParentheses !== false) {
|
||||
this.fileLintRules.push(hasMacroParentheses)
|
||||
}
|
||||
|
||||
if (json?.strictMacroDefinition) {
|
||||
if (json?.strictMacroDefinition !== false) {
|
||||
this.fileLintRules.push(strictMacroDefinition)
|
||||
}
|
||||
|
||||
if (json?.noGremlins !== false) {
|
||||
this.lineLintRules.push(noGremlins)
|
||||
}
|
||||
|
||||
if (json?.severityLevel) {
|
||||
for (const [rule, severity] of Object.entries(json.severityLevel)) {
|
||||
if (severity === 'warn') this.severityLevel[rule] = Severity.Warning
|
||||
|
||||
@@ -2,8 +2,8 @@ import * as fileModule from '@sasjs/utils/file'
|
||||
import { LintConfig } from '../types/LintConfig'
|
||||
import { getLintConfig } from './getLintConfig'
|
||||
|
||||
const expectedFileLintRulesCount = 5
|
||||
const expectedLineLintRulesCount = 5
|
||||
const expectedFileLintRulesCount = 6
|
||||
const expectedLineLintRulesCount = 6
|
||||
const expectedPathLintRulesCount = 2
|
||||
|
||||
describe('getLintConfig', () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ export const DefaultLintConfiguration = {
|
||||
noNestedMacros: true,
|
||||
hasMacroParentheses: true,
|
||||
strictMacroDefinition: true,
|
||||
noGremlins: true,
|
||||
defaultHeader: getDefaultHeader()
|
||||
}
|
||||
|
||||
|
||||
314
src/utils/gremlinCharacters.ts
Normal file
314
src/utils/gremlinCharacters.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
// Used https://compart.com/en/unicode to find the to find the description of each gremlin
|
||||
// List of gremlins was deduced from https://github.com/redoPop/SublimeGremlins/blob/main/Gremlins.py#L13
|
||||
|
||||
export const gremlinCharacters = {
|
||||
'0x0003': {
|
||||
description: 'End of Text'
|
||||
},
|
||||
'0x000b': {
|
||||
description: 'Line Tabulation'
|
||||
},
|
||||
'0x007f': {
|
||||
description: 'Delete'
|
||||
},
|
||||
'0x0080': {
|
||||
description: 'Padding'
|
||||
},
|
||||
'0x0081': {
|
||||
description: 'High Octet Preset'
|
||||
},
|
||||
'0x0082': {
|
||||
description: 'Break Permitted Here'
|
||||
},
|
||||
'0x0083': {
|
||||
description: 'No Break Here'
|
||||
},
|
||||
'0x0084': {
|
||||
description: 'Index'
|
||||
},
|
||||
'0x0085': {
|
||||
description: 'Next Line'
|
||||
},
|
||||
'0x0086': {
|
||||
description: 'Start of Selected Area'
|
||||
},
|
||||
'0x0087': {
|
||||
description: 'End of Selected Area'
|
||||
},
|
||||
'0x0088': {
|
||||
description: 'Character Tabulation Set'
|
||||
},
|
||||
'0x0089': {
|
||||
description: 'Character Tabulation with Justification'
|
||||
},
|
||||
'0x008a': {
|
||||
description: 'Line Tabulation Set'
|
||||
},
|
||||
'0x008b': {
|
||||
description: 'Partial Line Down'
|
||||
},
|
||||
'0x008c': {
|
||||
description: 'Partial Line Backward'
|
||||
},
|
||||
'0x008d': {
|
||||
description: 'Reverse Index'
|
||||
},
|
||||
'0x008e': {
|
||||
description: 'Single Shift Two'
|
||||
},
|
||||
'0x008f': {
|
||||
description: 'Single Shift Three'
|
||||
},
|
||||
'0x0090': {
|
||||
description: 'Device Control String'
|
||||
},
|
||||
'0x0091': {
|
||||
description: 'Private Use One'
|
||||
},
|
||||
'0x0092': {
|
||||
description: 'Private Use Two'
|
||||
},
|
||||
'0x0093': {
|
||||
description: 'Set Transmit State'
|
||||
},
|
||||
'0x0094': {
|
||||
description: 'Cancel Character'
|
||||
},
|
||||
'0x0095': {
|
||||
description: 'Message Waiting'
|
||||
},
|
||||
'0x0096': {
|
||||
description: 'Start of Guarded Area'
|
||||
},
|
||||
'0x0097': {
|
||||
description: 'End of Guarded Area'
|
||||
},
|
||||
'0x0098': {
|
||||
description: 'Start of String'
|
||||
},
|
||||
'0x0099': {
|
||||
description: 'Single Graphic Character Introducer'
|
||||
},
|
||||
'0x009a': {
|
||||
description: 'Single Character Introducer'
|
||||
},
|
||||
'0x009b': {
|
||||
description: 'Control Sequence Introducer'
|
||||
},
|
||||
'0x009c': {
|
||||
description: 'String Terminator'
|
||||
},
|
||||
'0x009d': {
|
||||
description: 'Operating System Command'
|
||||
},
|
||||
'0x009e': {
|
||||
description: 'Privacy Message'
|
||||
},
|
||||
'0x009f': {
|
||||
description: 'Application Program Command'
|
||||
},
|
||||
'0x00a0': {
|
||||
description: 'non breaking space'
|
||||
},
|
||||
'0x00ad': {
|
||||
description: 'Soft Hyphen'
|
||||
},
|
||||
'0x2000': {
|
||||
description: 'En Quad'
|
||||
},
|
||||
'0x2001': {
|
||||
description: 'Em Quad'
|
||||
},
|
||||
'0x2002': {
|
||||
description: 'En Space'
|
||||
},
|
||||
'0x2003': {
|
||||
description: 'Em Space'
|
||||
},
|
||||
'0x2004': {
|
||||
description: 'Three-Per-Em Space'
|
||||
},
|
||||
'0x2005': {
|
||||
description: 'Four-Per-Em Space'
|
||||
},
|
||||
'0x2006': {
|
||||
description: 'Six-Per-Em Space'
|
||||
},
|
||||
'0x2007': {
|
||||
description: 'Figure Space'
|
||||
},
|
||||
'0x2008': {
|
||||
description: 'Punctuation Space'
|
||||
},
|
||||
'0x2009': {
|
||||
description: 'Thin Space'
|
||||
},
|
||||
'0x200a': {
|
||||
description: 'Hair Space'
|
||||
},
|
||||
'0x200b': {
|
||||
description: 'Zero Width Space'
|
||||
},
|
||||
'0x200c': {
|
||||
description: 'Zero Width Non-Joiner'
|
||||
},
|
||||
'0x200d': {
|
||||
description: 'Zero Width Joiner'
|
||||
},
|
||||
'0x200e': {
|
||||
description: 'Left-to-Right Mark'
|
||||
},
|
||||
'0x200f': {
|
||||
description: 'Right-to-Left Mark'
|
||||
},
|
||||
'0x2013': {
|
||||
description: 'En Dash'
|
||||
},
|
||||
'0x2018': {
|
||||
description: 'Left Single Quotation Mark'
|
||||
},
|
||||
'0x2019': {
|
||||
description: 'Right Single Quotation Mark'
|
||||
},
|
||||
'0x201c': {
|
||||
description: 'Left Double Quotation Mark'
|
||||
},
|
||||
'0x201d': {
|
||||
description: 'Right Double Quotation Mark'
|
||||
},
|
||||
'0x2028': {
|
||||
description: 'Line Separator'
|
||||
},
|
||||
'0x2029': {
|
||||
description: 'Paragraph Separator'
|
||||
},
|
||||
'0x202a': {
|
||||
description: 'Left-to-Right Embedding'
|
||||
},
|
||||
'0x202b': {
|
||||
description: 'Right-to-Left Embedding'
|
||||
},
|
||||
'0x202c': {
|
||||
description: 'Pop Directional Formatting'
|
||||
},
|
||||
'0x202d': {
|
||||
description: 'Left-to-Right Override'
|
||||
},
|
||||
'0x202e': {
|
||||
description: 'Right-to-Left Override'
|
||||
},
|
||||
'0x202f': {
|
||||
description: 'Narrow No-Break Space'
|
||||
},
|
||||
'0x205f': {
|
||||
description: 'Medium Mathematical Space'
|
||||
},
|
||||
'0x2060': {
|
||||
description: 'Word Joiner'
|
||||
},
|
||||
'0x2061': {
|
||||
description: 'Function Application'
|
||||
},
|
||||
'0x2062': {
|
||||
description: 'Invisible Times'
|
||||
},
|
||||
'0x2063': {
|
||||
description: 'Invisible Separator'
|
||||
},
|
||||
'0x2064': {
|
||||
description: 'Invisible Plus'
|
||||
},
|
||||
'0x2066': {
|
||||
description: 'Left-to-Right Isolate'
|
||||
},
|
||||
'0x2067': {
|
||||
description: 'Right-to-Left Isolate'
|
||||
},
|
||||
'0x2068': {
|
||||
description: 'First Strong Isolate '
|
||||
},
|
||||
'0x2069': {
|
||||
description: 'Pop Directional Isolate'
|
||||
},
|
||||
'0x206a': {
|
||||
description: 'Inhibit Symmetric Swapping'
|
||||
},
|
||||
'0x206b': {
|
||||
description: 'Activate Symmetric Swapping'
|
||||
},
|
||||
'0x206c': {
|
||||
description: 'Inhibit Arabic Form Shaping'
|
||||
},
|
||||
'0x206d': {
|
||||
description: 'Activate Arabic Form Shaping'
|
||||
},
|
||||
'0x206e': {
|
||||
description: 'National Digit Shapes'
|
||||
},
|
||||
'0x206f': {
|
||||
description: 'Nominal Digit Shapes'
|
||||
},
|
||||
'0x2800': {
|
||||
description: 'Braille Pattern Blank'
|
||||
},
|
||||
'0x3000': {
|
||||
description: 'Ideographic Space'
|
||||
},
|
||||
'0x3164': {
|
||||
description: 'Hangul Filler'
|
||||
},
|
||||
'0xfe00': {
|
||||
description: 'Variation Selector-1'
|
||||
},
|
||||
'0xfe01': {
|
||||
description: 'Variation Selector-2'
|
||||
},
|
||||
'0xfe02': {
|
||||
description: 'Variation Selector-3'
|
||||
},
|
||||
'0xfe03': {
|
||||
description: 'Variation Selector-4'
|
||||
},
|
||||
'0xfe04': {
|
||||
description: 'Variation Selector-5'
|
||||
},
|
||||
'0xfe05': {
|
||||
description: 'Variation Selector-6'
|
||||
},
|
||||
'0xfe06': {
|
||||
description: 'Variation Selector-7'
|
||||
},
|
||||
'0xfe07': {
|
||||
description: 'Variation Selector-8'
|
||||
},
|
||||
'0xfe08': {
|
||||
description: 'Variation Selector-9'
|
||||
},
|
||||
'0xfe09': {
|
||||
description: 'Variation Selector-10'
|
||||
},
|
||||
'0xfe0a': {
|
||||
description: 'Variation Selector-11'
|
||||
},
|
||||
'0xfe0b': {
|
||||
description: 'Variation Selector-12 '
|
||||
},
|
||||
'0xfe0c': {
|
||||
description: 'Variation Selector-13'
|
||||
},
|
||||
'0xfe0d': {
|
||||
description: 'Variation Selector-14'
|
||||
},
|
||||
'0xfe0e': {
|
||||
description: 'Variation Selector-15'
|
||||
},
|
||||
'0xfe0f': {
|
||||
description: 'Variation Selector-16'
|
||||
},
|
||||
'0xfeff': {
|
||||
description: 'Zero Width No-Break Space'
|
||||
},
|
||||
'0xfffc': {
|
||||
description: 'Object Replacement Character'
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './asyncForEach'
|
||||
export * from './getLintConfig'
|
||||
export * from './getProjectRoot'
|
||||
export * from './gremlinCharacters'
|
||||
export * from './isIgnored'
|
||||
export * from './listSasFiles'
|
||||
export * from './splitText'
|
||||
|
||||
@@ -8,10 +8,19 @@ import { LineEndings } from '../types/LineEndings'
|
||||
*/
|
||||
export const splitText = (text: string, config: LintConfig): string[] => {
|
||||
if (!text) return []
|
||||
|
||||
const expectedLineEndings =
|
||||
config.lineEndings === LineEndings.LF ? '\n' : '\r\n'
|
||||
|
||||
const incorrectLineEndings = expectedLineEndings === '\n' ? '\r\n' : '\n'
|
||||
return text
|
||||
.replace(new RegExp(incorrectLineEndings, 'g'), expectedLineEndings)
|
||||
.split(expectedLineEndings)
|
||||
|
||||
text = text.replace(
|
||||
new RegExp(incorrectLineEndings, 'g'),
|
||||
expectedLineEndings
|
||||
)
|
||||
|
||||
// splitting text on '\r\n' was causing some problem
|
||||
// as it was retaining carriage return at the end of each line
|
||||
// so, removed the carriage returns from text and splitted on line feed (lf)
|
||||
return text.replace(/\r/g, '').split(/\n/)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user