mirror of
https://github.com/sasjs/lint.git
synced 2025-12-10 17:34:36 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6ddfa833d | ||
|
|
e227f16f88 | ||
|
|
7de907057d | ||
| 80c90ebda1 | |||
| c5ead229a9 | |||
| 7d6fc8eb8c | |||
|
|
65772804fe | ||
|
|
48a6628ec5 |
10
README.md
10
README.md
@@ -29,6 +29,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 +126,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/rules/line/noGremlins.ts](https://github.com/sasjs/lint/blob/main/src/rules/line/noGremlins.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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
128
src/rules/line/noGremlins.ts
Normal file
128
src/rules/line/noGremlins.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Diagnostic, LintConfig } from '../../types'
|
||||
import { LineLintRule } from '../../types/LintRule'
|
||||
import { LintRuleType } from '../../types/LintRuleType'
|
||||
import { Severity } from '../../types/Severity'
|
||||
|
||||
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))
|
||||
|
||||
const gremlinCharacters = {
|
||||
'0x2013': {
|
||||
description: 'en dash'
|
||||
},
|
||||
'0x2018': {
|
||||
description: 'left single quotation mark'
|
||||
},
|
||||
'0x2019': {
|
||||
description: 'right single quotation mark'
|
||||
},
|
||||
'0x2029': {
|
||||
zeroWidth: true,
|
||||
description: 'paragraph separator'
|
||||
},
|
||||
'0x2066': {
|
||||
zeroWidth: true,
|
||||
description: 'Left to right'
|
||||
},
|
||||
'0x2069': {
|
||||
zeroWidth: true,
|
||||
description: 'Pop directional'
|
||||
},
|
||||
'0x0003': {
|
||||
description: 'end of text'
|
||||
},
|
||||
'0x000b': {
|
||||
description: 'line tabulation'
|
||||
},
|
||||
'0x00a0': {
|
||||
description: 'non breaking space'
|
||||
},
|
||||
'0x00ad': {
|
||||
description: 'soft hyphen'
|
||||
},
|
||||
'0x200b': {
|
||||
zeroWidth: true,
|
||||
description: 'zero width space'
|
||||
},
|
||||
'0x200c': {
|
||||
zeroWidth: true,
|
||||
description: 'zero width non-joiner'
|
||||
},
|
||||
'0x200e': {
|
||||
zeroWidth: true,
|
||||
description: 'left-to-right mark'
|
||||
},
|
||||
'0x201c': {
|
||||
description: 'left double quotation mark'
|
||||
},
|
||||
'0x201d': {
|
||||
description: 'right double quotation mark'
|
||||
},
|
||||
'0x202c': {
|
||||
zeroWidth: true,
|
||||
description: 'pop directional formatting'
|
||||
},
|
||||
'0x202d': {
|
||||
zeroWidth: true,
|
||||
description: 'left-to-right override'
|
||||
},
|
||||
'0x202e': {
|
||||
zeroWidth: true,
|
||||
description: 'right-to-left override'
|
||||
},
|
||||
'0xfffc': {
|
||||
zeroWidth: true,
|
||||
description: 'object replacement character'
|
||||
}
|
||||
}
|
||||
@@ -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 =
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
maxLineLength,
|
||||
noEncodedPasswords,
|
||||
noTabs,
|
||||
noTrailingSpaces
|
||||
noTrailingSpaces,
|
||||
noGremlins
|
||||
} from '../rules/line'
|
||||
import { lowerCaseFileNames, noSpacesInFileNames } from '../rules/path'
|
||||
import { LineEndings } from './LineEndings'
|
||||
@@ -119,6 +120,10 @@ export class LintConfig {
|
||||
this.fileLintRules.push(strictMacroDefinition)
|
||||
}
|
||||
|
||||
if (json?.noGremlins) {
|
||||
this.lineLintRules.push(noGremlins)
|
||||
}
|
||||
|
||||
if (json?.severityLevel) {
|
||||
for (const [rule, severity] of Object.entries(json.severityLevel)) {
|
||||
if (severity === 'warn') this.severityLevel[rule] = Severity.Warning
|
||||
|
||||
Reference in New Issue
Block a user