From 3631f5c25cc4590024d96ccfdba7c7ecd1901593 Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Fri, 26 Mar 2021 09:09:42 +0000 Subject: [PATCH] feat(lint): add rules for lowercase file names, max line length and no tab indentation --- .sasjslint | 5 +++- sasjslint-schema.json | 34 +++++++++++++++++++-- src/example.ts | 18 +++++++++--- src/lint.ts | 2 +- src/rules/lowerCaseFileNames.spec.ts | 27 +++++++++++++++++ src/rules/lowerCaseFileNames.ts | 32 ++++++++++++++++++++ src/rules/maxLineLength.spec.ts | 44 ++++++++++++++++++++++++++++ src/rules/maxLineLength.ts | 32 ++++++++++++++++++++ src/rules/noSpacesInFileNames.ts | 2 +- src/rules/noTabIndentation.spec.ts | 22 ++++++++++++++ src/rules/noTabIndentation.ts | 30 +++++++++++++++++++ src/types/LintConfig.ts | 17 +++++++++++ src/types/LintRule.ts | 3 +- src/utils/getLintConfig.spec.ts | 3 +- src/utils/getLintConfig.ts | 6 +++- 15 files changed, 265 insertions(+), 12 deletions(-) create mode 100644 src/rules/lowerCaseFileNames.spec.ts create mode 100644 src/rules/lowerCaseFileNames.ts create mode 100644 src/rules/maxLineLength.spec.ts create mode 100644 src/rules/maxLineLength.ts create mode 100644 src/rules/noTabIndentation.spec.ts create mode 100644 src/rules/noTabIndentation.ts diff --git a/.sasjslint b/.sasjslint index 73e6a81..e87f1cf 100644 --- a/.sasjslint +++ b/.sasjslint @@ -2,5 +2,8 @@ "noTrailingSpaces": true, "noEncodedPasswords": true, "hasDoxygenHeader": true, - "noSpacesInFileNames": true + "noSpacesInFileNames": true, + "maxLineLength": 80, + "lowerCaseFileNames": true, + "noTabIndentation": true } \ No newline at end of file diff --git a/sasjslint-schema.json b/sasjslint-schema.json index c7e519a..fcecdc3 100644 --- a/sasjslint-schema.json +++ b/sasjslint-schema.json @@ -8,14 +8,20 @@ "noTrailingSpaces": true, "noEncodedPasswords": true, "hasDoxygenHeader": true, - "noSpacesInFileNames": true + "noSpacesInFileNames": true, + "lowerCaseFileNames": true, + "maxLineLength": 80, + "noTabIndentation": true }, "examples": [ { "noTrailingSpaces": true, "noEncodedPasswords": true, "hasDoxygenHeader": true, - "noSpacesInFileNames": true + "noSpacesInFileNames": true, + "lowerCaseFileNames": true, + "maxLineLength": 80, + "noTabIndentation": true } ], "properties": { @@ -50,6 +56,30 @@ "description": "Enforces no spaces in file names. Shows a warning when they are present.", "default": true, "examples": [true, false] + }, + "lowerCaseFileNames": { + "$id": "#/properties/lowerCaseFileNames", + "type": "boolean", + "title": "lowerCaseFileNames", + "description": "Enforces no uppercase characters in file names. Shows a warning when they are present.", + "default": true, + "examples": [true, false] + }, + "maxLineLength": { + "$id": "#/properties/maxLineLength", + "type": "number", + "title": "maxLineLength", + "description": "Enforces a configurable maximum line length. Shows a warning for lines exceeding this length.", + "default": 80, + "examples": [60, 80, 120] + }, + "noTabIndentation": { + "$id": "#/properties/noTabIndentation", + "type": "boolean", + "title": "noTabIndentation", + "description": "Enforces no indentation using tabs. Shows a warning when a line starts with a tab.", + "default": true, + "examples": [true, false] } } } diff --git a/src/example.ts b/src/example.ts index 22a9a14..dd25f2b 100644 --- a/src/example.ts +++ b/src/example.ts @@ -1,4 +1,5 @@ -import { lintText } from './lint' +import { lintFile, lintText } from './lint' +import path from 'path' /** * Example which tests a piece of text with all known violations. @@ -30,8 +31,8 @@ const text = `/** %macro mf_getuniquelibref(prefix=mclib,maxtries=1000); - %local x libref; - %let x={SAS002}; + %local x libref; + %let x={SAS002}; %do x=0 %to &maxtries; %if %sysfunc(libref(&prefix&x)) ne 0 %then %do; %let libref=&prefix&x; @@ -44,6 +45,15 @@ const text = `/** %end; %put unable to find available libref in range &prefix.0-&maxtries; %mend; + ` -lintText(text).then((diagnostics) => console.table(diagnostics)) +lintText(text).then((diagnostics) => { + console.log('Text lint results:') + console.table(diagnostics) +}) + +lintFile(path.join(__dirname, 'Example File.sas')).then((diagnostics) => { + console.log('File lint results:') + console.table(diagnostics) +}) diff --git a/src/lint.ts b/src/lint.ts index 5070def..8aba8df 100644 --- a/src/lint.ts +++ b/src/lint.ts @@ -65,7 +65,7 @@ const processLine = ( ): Diagnostic[] => { const diagnostics: Diagnostic[] = [] config.lineLintRules.forEach((rule) => { - diagnostics.push(...rule.test(line, lineNumber)) + diagnostics.push(...rule.test(line, lineNumber, config)) }) return diagnostics diff --git a/src/rules/lowerCaseFileNames.spec.ts b/src/rules/lowerCaseFileNames.spec.ts new file mode 100644 index 0000000..4ecaddb --- /dev/null +++ b/src/rules/lowerCaseFileNames.spec.ts @@ -0,0 +1,27 @@ +import { Severity } from '../types/Severity' +import { lowerCaseFileNames } from './lowerCaseFileNames' + +describe('lowerCaseFileNames', () => { + it('should return an empty array when the file name has no uppercase characters', () => { + const filePath = '/code/sas/my_sas_file.sas' + expect(lowerCaseFileNames.test(filePath)).toEqual([]) + }) + + it('should return an empty array when the file name has no uppercase characters, even if the containing folder has uppercase characters', () => { + const filePath = '/code/SAS Projects/my_sas_file.sas' + expect(lowerCaseFileNames.test(filePath)).toEqual([]) + }) + + it('should return an array with a single diagnostic when the file name has uppercase characters', () => { + const filePath = '/code/sas/my SAS file.sas' + expect(lowerCaseFileNames.test(filePath)).toEqual([ + { + message: 'File name contains uppercase characters', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + } + ]) + }) +}) diff --git a/src/rules/lowerCaseFileNames.ts b/src/rules/lowerCaseFileNames.ts new file mode 100644 index 0000000..2eb8e51 --- /dev/null +++ b/src/rules/lowerCaseFileNames.ts @@ -0,0 +1,32 @@ +import { PathLintRule } from '../types/LintRule' +import { LintRuleType } from '../types/LintRuleType' +import { Severity } from '../types/Severity' +import path from 'path' + +const name = 'lowerCaseFileNames' +const description = 'Enforce the use of lower case file names.' +const message = 'File name contains uppercase characters' +const test = (value: string) => { + const fileName = path.basename(value) + if (fileName.toLocaleLowerCase() === fileName) return [] + return [ + { + message, + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + } + ] +} + +/** + * Lint rule that checks for the absence of uppercase characters in a given file name. + */ +export const lowerCaseFileNames: PathLintRule = { + type: LintRuleType.Path, + name, + description, + message, + test +} diff --git a/src/rules/maxLineLength.spec.ts b/src/rules/maxLineLength.spec.ts new file mode 100644 index 0000000..c763d26 --- /dev/null +++ b/src/rules/maxLineLength.spec.ts @@ -0,0 +1,44 @@ +import { LintConfig, Severity } from '../types' +import { maxLineLength } from './maxLineLength' + +describe('maxLineLength', () => { + it('should return an empty array when the line is within the specified length', () => { + const line = "%put 'hello';" + const config = new LintConfig({ maxLineLength: 60 }) + expect(maxLineLength.test(line, 1, config)).toEqual([]) + }) + + it('should return an array with a single diagnostic when the line exceeds the specified length', () => { + const line = "%put 'hello';" + const config = new LintConfig({ maxLineLength: 10 }) + expect(maxLineLength.test(line, 1, config)).toEqual([ + { + message: `Line exceeds maximum length by 3 characters`, + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + } + ]) + }) + + it('should fall back to a default of 80 characters', () => { + const line = + 'Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yardarm. Pinnace holystone.' + expect(maxLineLength.test(line, 1)).toEqual([ + { + message: `Line exceeds maximum length by 15 characters`, + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + } + ]) + }) + + it('should return an empty array for lines within the default length', () => { + const line = + 'Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yard' + expect(maxLineLength.test(line, 1)).toEqual([]) + }) +}) diff --git a/src/rules/maxLineLength.ts b/src/rules/maxLineLength.ts new file mode 100644 index 0000000..8d054c7 --- /dev/null +++ b/src/rules/maxLineLength.ts @@ -0,0 +1,32 @@ +import { LintConfig } from '../types' +import { LineLintRule } from '../types/LintRule' +import { LintRuleType } from '../types/LintRuleType' +import { Severity } from '../types/Severity' + +const name = 'maxLineLength' +const description = 'Restrict lines to the specified length.' +const message = 'Line exceeds maximum length' +const test = (value: string, lineNumber: number, config?: LintConfig) => { + const maxLineLength = config?.maxLineLength || 80 + if (value.length <= maxLineLength) return [] + return [ + { + message: `${message} by ${value.length - maxLineLength} characters`, + lineNumber, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + } + ] +} + +/** + * Lint rule that checks if a line has exceeded the configured maximum length. + */ +export const maxLineLength: LineLintRule = { + type: LintRuleType.Line, + name, + description, + message, + test +} diff --git a/src/rules/noSpacesInFileNames.ts b/src/rules/noSpacesInFileNames.ts index cf9eb46..6723ea3 100644 --- a/src/rules/noSpacesInFileNames.ts +++ b/src/rules/noSpacesInFileNames.ts @@ -1,7 +1,7 @@ import { PathLintRule } from '../types/LintRule' import { LintRuleType } from '../types/LintRuleType' -import path from 'path' import { Severity } from '../types/Severity' +import path from 'path' const name = 'noSpacesInFileNames' const description = 'Enforce the absence of spaces within file names.' diff --git a/src/rules/noTabIndentation.spec.ts b/src/rules/noTabIndentation.spec.ts new file mode 100644 index 0000000..ea300a8 --- /dev/null +++ b/src/rules/noTabIndentation.spec.ts @@ -0,0 +1,22 @@ +import { Severity } from '../types/Severity' +import { noTabIndentation } from './noTabIndentation' + +describe('noTabs', () => { + it('should return an empty array when the line is not indented with a tab', () => { + const line = "%put 'hello';" + expect(noTabIndentation.test(line, 1)).toEqual([]) + }) + + it('should return an array with a single diagnostic when the line is indented with a tab', () => { + const line = "\t%put 'hello';" + expect(noTabIndentation.test(line, 1)).toEqual([ + { + message: 'Line is indented with a tab', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + } + ]) + }) +}) diff --git a/src/rules/noTabIndentation.ts b/src/rules/noTabIndentation.ts new file mode 100644 index 0000000..af2a46b --- /dev/null +++ b/src/rules/noTabIndentation.ts @@ -0,0 +1,30 @@ +import { LineLintRule } from '../types/LintRule' +import { LintRuleType } from '../types/LintRuleType' +import { Severity } from '../types/Severity' + +const name = 'noTabs' +const description = 'Disallow indenting with tabs.' +const message = 'Line is indented with a tab' +const test = (value: string, lineNumber: number) => { + if (!value.startsWith('\t')) return [] + return [ + { + message, + lineNumber, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + } + ] +} + +/** + * Lint rule that checks if a given line of text is indented with a tab. + */ +export const noTabIndentation: LineLintRule = { + type: LintRuleType.Line, + name, + description, + message, + test +} diff --git a/src/types/LintConfig.ts b/src/types/LintConfig.ts index ab65991..91c0d08 100644 --- a/src/types/LintConfig.ts +++ b/src/types/LintConfig.ts @@ -1,6 +1,9 @@ import { hasDoxygenHeader } from '../rules/hasDoxygenHeader' +import { lowerCaseFileNames } from '../rules/lowerCaseFileNames' +import { maxLineLength } from '../rules/maxLineLength' import { noEncodedPasswords } from '../rules/noEncodedPasswords' import { noSpacesInFileNames } from '../rules/noSpacesInFileNames' +import { noTabIndentation } from '../rules/noTabIndentation' import { noTrailingSpaces } from '../rules/noTrailingSpaces' import { FileLintRule, LineLintRule, PathLintRule } from './LintRule' @@ -15,6 +18,7 @@ export class LintConfig { readonly lineLintRules: LineLintRule[] = [] readonly fileLintRules: FileLintRule[] = [] readonly pathLintRules: PathLintRule[] = [] + readonly maxLineLength = 80 constructor(json?: any) { if (json?.noTrailingSpaces) { @@ -25,6 +29,15 @@ export class LintConfig { this.lineLintRules.push(noEncodedPasswords) } + if (json?.noTabIndentation) { + this.lineLintRules.push(noTabIndentation) + } + + if (json?.maxLineLength) { + this.maxLineLength = json.maxLineLength + this.lineLintRules.push(maxLineLength) + } + if (json?.hasDoxygenHeader) { this.fileLintRules.push(hasDoxygenHeader) } @@ -32,5 +45,9 @@ export class LintConfig { if (json?.noSpacesInFileNames) { this.pathLintRules.push(noSpacesInFileNames) } + + if (json?.lowerCaseFileNames) { + this.pathLintRules.push(lowerCaseFileNames) + } } } diff --git a/src/types/LintRule.ts b/src/types/LintRule.ts index bb2bcb0..d3fbf29 100644 --- a/src/types/LintRule.ts +++ b/src/types/LintRule.ts @@ -1,4 +1,5 @@ import { Diagnostic } from './Diagnostic' +import { LintConfig } from './LintConfig' import { LintRuleType } from './LintRuleType' /** @@ -17,7 +18,7 @@ export interface LintRule { */ export interface LineLintRule extends LintRule { type: LintRuleType.Line - test: (value: string, lineNumber: number) => Diagnostic[] + test: (value: string, lineNumber: number, config?: LintConfig) => Diagnostic[] } /** diff --git a/src/utils/getLintConfig.spec.ts b/src/utils/getLintConfig.spec.ts index d5136d4..8f9f64d 100644 --- a/src/utils/getLintConfig.spec.ts +++ b/src/utils/getLintConfig.spec.ts @@ -18,6 +18,7 @@ describe('getLintConfig', () => { expect(config).toBeInstanceOf(LintConfig) expect(config.fileLintRules.length).toEqual(1) - expect(config.lineLintRules.length).toEqual(2) + expect(config.lineLintRules.length).toEqual(4) + expect(config.pathLintRules.length).toEqual(2) }) }) diff --git a/src/utils/getLintConfig.ts b/src/utils/getLintConfig.ts index 97178cf..435f800 100644 --- a/src/utils/getLintConfig.ts +++ b/src/utils/getLintConfig.ts @@ -6,7 +6,11 @@ import { getProjectRoot } from './getProjectRoot' const defaultConfiguration = { noTrailingSpaces: true, noEncodedPasswords: true, - hasDoxygenHeader: true + hasDoxygenHeader: true, + noSpacesInFileNames: true, + lowerCaseFileNames: true, + maxLineLength: 80, + noTabIndentation: true } /** * Fetches the config from the .sasjslint file and creates a LintConfig object.