diff --git a/sasjslint-schema.json b/sasjslint-schema.json index e47b6c4..ea853c4 100644 --- a/sasjslint-schema.json +++ b/sasjslint-schema.json @@ -64,6 +64,14 @@ "default": "/**{lineEnding} @file{lineEnding} @brief {lineEnding}

SAS Macros

{lineEnding}**/", "examples": [] }, + "noGremlins": { + "$id": "#/properties/noGremlins", + "type": "array", + "title": "noGremlins", + "description": "", + "default": [true], + "examples": [true, false] + }, "hasMacroNameInMend": { "$id": "#/properties/hasMacroNameInMend", "type": "boolean", @@ -193,6 +201,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", diff --git a/src/rules/line/index.ts b/src/rules/line/index.ts index a61f46c..c012e22 100644 --- a/src/rules/line/index.ts +++ b/src/rules/line/index.ts @@ -1,3 +1,4 @@ +export { noGremlins } from './noGremlins' export { indentationMultiple } from './indentationMultiple' export { maxLineLength } from './maxLineLength' export { noEncodedPasswords } from './noEncodedPasswords' diff --git a/src/rules/line/noGremlins.ts b/src/rules/line/noGremlins.ts new file mode 100644 index 0000000..9b38539 --- /dev/null +++ b/src/rules/line/noGremlins.ts @@ -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 grimlins array' +const message = 'Line contains a grimlin character' + +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 grimlin. + */ +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' + } +} diff --git a/src/types/LintConfig.ts b/src/types/LintConfig.ts index 5a9c6e7..b532d00 100644 --- a/src/types/LintConfig.ts +++ b/src/types/LintConfig.ts @@ -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