diff --git a/.sasjslint b/.sasjslint index e87f1cf..44e07e4 100644 --- a/.sasjslint +++ b/.sasjslint @@ -5,5 +5,6 @@ "noSpacesInFileNames": true, "maxLineLength": 80, "lowerCaseFileNames": true, - "noTabIndentation": true + "noTabIndentation": true, + "indentationMultiple": 2 } \ No newline at end of file diff --git a/src/Example File.sas b/src/Example File.sas new file mode 100644 index 0000000..4056077 --- /dev/null +++ b/src/Example File.sas @@ -0,0 +1,18 @@ + + +%macro mf_getuniquelibref(prefix=mclib,maxtries=1000); + %local x libref; + %let x={SAS002}; + %do x=0 %to &maxtries; + %if %sysfunc(libref(&prefix&x)) ne 0 %then %do; + %let libref=&prefix&x; + %let rc=%sysfunc(libname(&libref,%sysfunc(pathname(work)))); + %if &rc %then %put %sysfunc(sysmsg()); + &prefix&x + %*put &sysmacroname: Libref &libref assigned as WORK and returned; + %return; + %end; + %end; + %put unable to find available libref in range &prefix.0-&maxtries; + %mend; + diff --git a/src/example file.sas b/src/example file.sas deleted file mode 100644 index 3ff3243..0000000 --- a/src/example file.sas +++ /dev/null @@ -1,17 +0,0 @@ - - - %macro mf_getuniquelibref(prefix=mclib,maxtries=1000); - %local x libref; - %let x={SAS002}; - %do x=0 %to &maxtries; - %if %sysfunc(libref(&prefix&x)) ne 0 %then %do; - %let libref=&prefix&x; - %let rc=%sysfunc(libname(&libref,%sysfunc(pathname(work)))); - %if &rc %then %put %sysfunc(sysmsg()); - &prefix&x - %*put &sysmacroname: Libref &libref assigned as WORK and returned; - %return; - %end; - %end; - %put unable to find available libref in range &prefix.0-&maxtries; - %mend; \ No newline at end of file diff --git a/src/example.ts b/src/example.ts index dd25f2b..bae4de8 100644 --- a/src/example.ts +++ b/src/example.ts @@ -6,46 +6,45 @@ import path from 'path' */ const text = `/** - @file - @brief Returns an unused libref - @details Use as follows: + @file + @brief Returns an unused libref + @details Use as follows: - libname mclib0 (work); - libname mclib1 (work); - libname mclib2 (work); + libname mclib0 (work); + libname mclib1 (work); + libname mclib2 (work); - %let libref=%mf_getuniquelibref({SAS001}); - %put &=libref; + %let libref=%mf_getuniquelibref({SAS001}); + %put &=libref; - which returns: + which returns: > mclib3 - @param prefix= first part of libref. Remember that librefs can only be 8 characters, - so a 7 letter prefix would mean that maxtries should be 10. - @param maxtries= the last part of the libref. Provide an integer value. + @param prefix= first part of libref. Remember that librefs can only be 8 characters, + so a 7 letter prefix would mean that maxtries should be 10. + @param maxtries= the last part of the libref. Provide an integer value. - @version 9.2 - @author Allan Bowe + @version 9.2 + @author Allan Bowe **/ - %macro mf_getuniquelibref(prefix=mclib,maxtries=1000); - %local x libref; - %let x={SAS002}; +%macro mf_getuniquelibref(prefix=mclib,maxtries=1000); + %local x libref; + %let x={SAS002}; %do x=0 %to &maxtries; - %if %sysfunc(libref(&prefix&x)) ne 0 %then %do; - %let libref=&prefix&x; - %let rc=%sysfunc(libname(&libref,%sysfunc(pathname(work)))); - %if &rc %then %put %sysfunc(sysmsg()); - &prefix&x - %*put &sysmacroname: Libref &libref assigned as WORK and returned; - %return; - %end; - %end; - %put unable to find available libref in range &prefix.0-&maxtries; - %mend; - + %if %sysfunc(libref(&prefix&x)) ne 0 %then %do; + %let libref=&prefix&x; + %let rc=%sysfunc(libname(&libref,%sysfunc(pathname(work)))); + %if &rc %then %put %sysfunc(sysmsg()); + &prefix&x + %*put &sysmacroname: Libref &libref assigned as WORK and returned; + %return; + %end; + %end; + %put unable to find available libref in range &prefix.0-&maxtries; + %mend; ` lintText(text).then((diagnostics) => { diff --git a/src/lint.spec.ts b/src/lint.spec.ts index 9b6f5e2..d6d5e21 100644 --- a/src/lint.spec.ts +++ b/src/lint.spec.ts @@ -71,9 +71,9 @@ describe('lintText', () => { describe('lintFile', () => { it('should identify lint issues in a given file', async () => { - const results = await lintFile(path.join(__dirname, 'example file.sas')) + const results = await lintFile(path.join(__dirname, 'Example File.sas')) - expect(results.length).toEqual(5) + expect(results.length).toEqual(8) expect(results).toContainEqual({ message: 'Line contains trailing spaces', lineNumber: 1, @@ -95,6 +95,13 @@ describe('lintFile', () => { endColumnNumber: 1, severity: Severity.Warning }) + expect(results).toContainEqual({ + message: 'File name contains uppercase characters', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) expect(results).toContainEqual({ message: 'File missing Doxygen header', lineNumber: 1, @@ -105,10 +112,24 @@ describe('lintFile', () => { expect(results).toContainEqual({ message: 'Line contains encoded password', lineNumber: 5, - startColumnNumber: 11, - endColumnNumber: 19, + startColumnNumber: 10, + endColumnNumber: 18, severity: Severity.Error }) + expect(results).toContainEqual({ + message: 'Line is indented with a tab', + lineNumber: 7, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) + expect(results).toContainEqual({ + message: 'Line has incorrect indentation - 3 spaces', + lineNumber: 6, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) }) }) diff --git a/src/rules/indentationMultiple.spec.ts b/src/rules/indentationMultiple.spec.ts new file mode 100644 index 0000000..830c055 --- /dev/null +++ b/src/rules/indentationMultiple.spec.ts @@ -0,0 +1,68 @@ +import { LintConfig, Severity } from '../types' +import { indentationMultiple } from './indentationMultiple' + +describe('indentationMultiple', () => { + it('should return an empty array when the line is indented by two spaces', () => { + const line = " %put 'hello';" + const config = new LintConfig({ indentationMultiple: 2 }) + expect(indentationMultiple.test(line, 1, config)).toEqual([]) + }) + + it('should return an empty array when the line is indented by a multiple of 2 spaces', () => { + const line = " %put 'hello';" + const config = new LintConfig({ indentationMultiple: 2 }) + expect(indentationMultiple.test(line, 1, config)).toEqual([]) + }) + + it('should return an empty array when the line is not indented', () => { + const line = "%put 'hello';" + const config = new LintConfig({ indentationMultiple: 2 }) + expect(indentationMultiple.test(line, 1, config)).toEqual([]) + }) + + it('should return an array with a single diagnostic when the line is indented incorrectly', () => { + const line = " %put 'hello';" + const config = new LintConfig({ indentationMultiple: 2 }) + expect(indentationMultiple.test(line, 1, config)).toEqual([ + { + message: `Line has incorrect indentation - 3 spaces`, + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + } + ]) + }) + + it('should return an array with a single diagnostic when the line is indented incorrectly', () => { + const line = " %put 'hello';" + const config = new LintConfig({ indentationMultiple: 3 }) + expect(indentationMultiple.test(line, 1, config)).toEqual([ + { + message: `Line has incorrect indentation - 2 spaces`, + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + } + ]) + }) + + it('should fall back to a default of 2 spaces', () => { + const line = " %put 'hello';" + expect(indentationMultiple.test(line, 1)).toEqual([ + { + message: `Line has incorrect indentation - 1 space`, + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + } + ]) + }) + + it('should return an empty array for lines within the default indentation', () => { + const line = " %put 'hello';" + expect(indentationMultiple.test(line, 1)).toEqual([]) + }) +}) diff --git a/src/rules/indentationMultiple.ts b/src/rules/indentationMultiple.ts new file mode 100644 index 0000000..dee096d --- /dev/null +++ b/src/rules/indentationMultiple.ts @@ -0,0 +1,37 @@ +import { LintConfig } from '../types' +import { LineLintRule } from '../types/LintRule' +import { LintRuleType } from '../types/LintRuleType' +import { Severity } from '../types/Severity' + +const name = 'indentationMultiple' +const description = 'Ensure indentation by a multiple of the configured number.' +const message = 'Line has incorrect indentation' +const test = (value: string, lineNumber: number, config?: LintConfig) => { + if (!value.startsWith(' ')) return [] + + const indentationMultiple = config?.indentationMultiple || 2 + const numberOfSpaces = value.search(/\S|$/) + if (numberOfSpaces % indentationMultiple === 0) return [] + return [ + { + message: `${message} - ${numberOfSpaces} ${ + numberOfSpaces === 1 ? 'space' : 'spaces' + }`, + lineNumber, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + } + ] +} + +/** + * Lint rule that checks if a line is indented by a multiple of the configured indentation multiple. + */ +export const indentationMultiple: LineLintRule = { + type: LintRuleType.Line, + name, + description, + message, + test +} diff --git a/src/types/LintConfig.ts b/src/types/LintConfig.ts index 91c0d08..f83de33 100644 --- a/src/types/LintConfig.ts +++ b/src/types/LintConfig.ts @@ -1,4 +1,5 @@ import { hasDoxygenHeader } from '../rules/hasDoxygenHeader' +import { indentationMultiple } from '../rules/indentationMultiple' import { lowerCaseFileNames } from '../rules/lowerCaseFileNames' import { maxLineLength } from '../rules/maxLineLength' import { noEncodedPasswords } from '../rules/noEncodedPasswords' @@ -19,6 +20,7 @@ export class LintConfig { readonly fileLintRules: FileLintRule[] = [] readonly pathLintRules: PathLintRule[] = [] readonly maxLineLength = 80 + readonly indentationMultiple = 2 constructor(json?: any) { if (json?.noTrailingSpaces) { @@ -38,6 +40,11 @@ export class LintConfig { this.lineLintRules.push(maxLineLength) } + if (json?.indentationMultiple) { + this.indentationMultiple = json.indentationMultiple + this.lineLintRules.push(indentationMultiple) + } + if (json?.hasDoxygenHeader) { this.fileLintRules.push(hasDoxygenHeader) } diff --git a/src/utils/getLintConfig.spec.ts b/src/utils/getLintConfig.spec.ts index 8f9f64d..e7e4870 100644 --- a/src/utils/getLintConfig.spec.ts +++ b/src/utils/getLintConfig.spec.ts @@ -18,7 +18,7 @@ describe('getLintConfig', () => { expect(config).toBeInstanceOf(LintConfig) expect(config.fileLintRules.length).toEqual(1) - expect(config.lineLintRules.length).toEqual(4) + expect(config.lineLintRules.length).toEqual(5) expect(config.pathLintRules.length).toEqual(2) }) }) diff --git a/src/utils/getLintConfig.ts b/src/utils/getLintConfig.ts index be39867..c90b15d 100644 --- a/src/utils/getLintConfig.ts +++ b/src/utils/getLintConfig.ts @@ -13,7 +13,8 @@ export const DefaultLintConfiguration = { noSpacesInFileNames: true, lowerCaseFileNames: true, maxLineLength: 80, - noTabIndentation: true + noTabIndentation: true, + indentationMultiple: 2 } /**