Skip to content

Commit

Permalink
Merge pull request #10 from sasjs/add-lint-rules
Browse files Browse the repository at this point in the history
  • Loading branch information
krishna-acondy authored Mar 29, 2021
2 parents 1be358c + 2ad4263 commit 7a2e693
Show file tree
Hide file tree
Showing 23 changed files with 490 additions and 61 deletions.
6 changes: 5 additions & 1 deletion .sasjslint
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@
"noTrailingSpaces": true,
"noEncodedPasswords": true,
"hasDoxygenHeader": true,
"noSpacesInFileNames": true
"noSpacesInFileNames": true,
"maxLineLength": 80,
"lowerCaseFileNames": true,
"noTabIndentation": true,
"indentationMultiple": 2
}
44 changes: 42 additions & 2 deletions sasjslint-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@
"noTrailingSpaces": true,
"noEncodedPasswords": true,
"hasDoxygenHeader": true,
"noSpacesInFileNames": true
"noSpacesInFileNames": true,
"lowerCaseFileNames": true,
"maxLineLength": 80,
"noTabIndentation": true,
"indentationMultiple": 2
},
"examples": [
{
"noTrailingSpaces": true,
"noEncodedPasswords": true,
"hasDoxygenHeader": true,
"noSpacesInFileNames": true
"noSpacesInFileNames": true,
"lowerCaseFileNames": true,
"maxLineLength": 80,
"noTabIndentation": true,
"indentationMultiple": 4
}
],
"properties": {
Expand Down Expand Up @@ -50,6 +58,38 @@
"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]
},
"indentationMultiple": {
"$id": "#/properties/indentationMultiple",
"type": "number",
"title": "indentationMultiple",
"description": "Enforces a configurable multiple for the number of spaces for indentation. Shows a warning for lines that are not indented by a multiple of this number.",
"default": 2,
"examples": [2, 3, 4]
}
}
}
18 changes: 18 additions & 0 deletions src/Example File.sas
Original file line number Diff line number Diff line change
@@ -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;

17 changes: 0 additions & 17 deletions src/example file.sas

This file was deleted.

69 changes: 39 additions & 30 deletions src/example.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,58 @@
import { lintText } from './lint'
import { lintFile, lintText } from './lint'
import path from 'path'

/**
* Example which tests a piece of text with all known violations.
*/

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) => 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)
})
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { lintText, lintFile } from './lint'
export * from './types'
export * from './utils'
29 changes: 25 additions & 4 deletions src/lint.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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
})
})
})

Expand Down
2 changes: 1 addition & 1 deletion src/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions src/rules/indentationMultiple.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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 ignore indentation when the multiple is set to 0', () => {
const line = " %put 'hello';"
const config = new LintConfig({ indentationMultiple: 0 })
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([])
})
})
41 changes: 41 additions & 0 deletions src/rules/indentationMultiple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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 = isNaN(config?.indentationMultiple as number)
? 2
: config?.indentationMultiple

if (indentationMultiple === 0) return []
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
}
Loading

0 comments on commit 7a2e693

Please sign in to comment.