mirror of
https://github.com/sasjs/lint.git
synced 2025-12-10 17:34:36 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12bfcd69bd | ||
|
|
6350d32d0c | ||
|
|
1c09a10290 | ||
|
|
7a2e693123 | ||
|
|
2ad42634d7 | ||
|
|
52b63bac58 | ||
|
|
f1adcb8cb4 | ||
|
|
8fc3c39993 | ||
|
|
3631f5c25c |
@@ -2,5 +2,9 @@
|
|||||||
"noTrailingSpaces": true,
|
"noTrailingSpaces": true,
|
||||||
"noEncodedPasswords": true,
|
"noEncodedPasswords": true,
|
||||||
"hasDoxygenHeader": true,
|
"hasDoxygenHeader": true,
|
||||||
"noSpacesInFileNames": true
|
"noSpacesInFileNames": true,
|
||||||
|
"maxLineLength": 80,
|
||||||
|
"lowerCaseFileNames": true,
|
||||||
|
"noTabIndentation": true,
|
||||||
|
"indentationMultiple": 2
|
||||||
}
|
}
|
||||||
@@ -8,14 +8,22 @@
|
|||||||
"noTrailingSpaces": true,
|
"noTrailingSpaces": true,
|
||||||
"noEncodedPasswords": true,
|
"noEncodedPasswords": true,
|
||||||
"hasDoxygenHeader": true,
|
"hasDoxygenHeader": true,
|
||||||
"noSpacesInFileNames": true
|
"noSpacesInFileNames": true,
|
||||||
|
"lowerCaseFileNames": true,
|
||||||
|
"maxLineLength": 80,
|
||||||
|
"noTabIndentation": true,
|
||||||
|
"indentationMultiple": 2
|
||||||
},
|
},
|
||||||
"examples": [
|
"examples": [
|
||||||
{
|
{
|
||||||
"noTrailingSpaces": true,
|
"noTrailingSpaces": true,
|
||||||
"noEncodedPasswords": true,
|
"noEncodedPasswords": true,
|
||||||
"hasDoxygenHeader": true,
|
"hasDoxygenHeader": true,
|
||||||
"noSpacesInFileNames": true
|
"noSpacesInFileNames": true,
|
||||||
|
"lowerCaseFileNames": true,
|
||||||
|
"maxLineLength": 80,
|
||||||
|
"noTabIndentation": true,
|
||||||
|
"indentationMultiple": 4
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -50,6 +58,38 @@
|
|||||||
"description": "Enforces no spaces in file names. Shows a warning when they are present.",
|
"description": "Enforces no spaces in file names. Shows a warning when they are present.",
|
||||||
"default": true,
|
"default": true,
|
||||||
"examples": [true, false]
|
"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
src/Example File.sas
Normal file
18
src/Example File.sas
Normal file
@@ -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;
|
||||||
|
|
||||||
@@ -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;
|
|
||||||
@@ -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.
|
* Example which tests a piece of text with all known violations.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const text = `/**
|
const text = `/**
|
||||||
@file
|
@file
|
||||||
@brief Returns an unused libref
|
@brief Returns an unused libref
|
||||||
@details Use as follows:
|
@details Use as follows:
|
||||||
|
|
||||||
libname mclib0 (work);
|
libname mclib0 (work);
|
||||||
libname mclib1 (work);
|
libname mclib1 (work);
|
||||||
libname mclib2 (work);
|
libname mclib2 (work);
|
||||||
|
|
||||||
%let libref=%mf_getuniquelibref({SAS001});
|
%let libref=%mf_getuniquelibref({SAS001});
|
||||||
%put &=libref;
|
%put &=libref;
|
||||||
|
|
||||||
which returns:
|
which returns:
|
||||||
|
|
||||||
> mclib3
|
> mclib3
|
||||||
|
|
||||||
@param prefix= first part of libref. Remember that librefs can only be 8 characters,
|
@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.
|
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 maxtries= the last part of the libref. Provide an integer value.
|
||||||
|
|
||||||
@version 9.2
|
@version 9.2
|
||||||
@author Allan Bowe
|
@author Allan Bowe
|
||||||
**/
|
**/
|
||||||
|
|
||||||
|
|
||||||
%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
|
%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
|
||||||
%local x libref;
|
%local x libref;
|
||||||
%let x={SAS002};
|
%let x={SAS002};
|
||||||
%do x=0 %to &maxtries;
|
%do x=0 %to &maxtries;
|
||||||
%if %sysfunc(libref(&prefix&x)) ne 0 %then %do;
|
%if %sysfunc(libref(&prefix&x)) ne 0 %then %do;
|
||||||
%let libref=&prefix&x;
|
%let libref=&prefix&x;
|
||||||
%let rc=%sysfunc(libname(&libref,%sysfunc(pathname(work))));
|
%let rc=%sysfunc(libname(&libref,%sysfunc(pathname(work))));
|
||||||
%if &rc %then %put %sysfunc(sysmsg());
|
%if &rc %then %put %sysfunc(sysmsg());
|
||||||
&prefix&x
|
&prefix&x
|
||||||
%*put &sysmacroname: Libref &libref assigned as WORK and returned;
|
%*put &sysmacroname: Libref &libref assigned as WORK and returned;
|
||||||
%return;
|
%return;
|
||||||
%end;
|
%end;
|
||||||
%end;
|
%end;
|
||||||
%put unable to find available libref in range &prefix.0-&maxtries;
|
%put unable to find available libref in range &prefix.0-&maxtries;
|
||||||
%mend;
|
%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,2 +1,3 @@
|
|||||||
export { lintText, lintFile } from './lint'
|
export { lintText, lintFile } from './lint'
|
||||||
export * from './types'
|
export * from './types'
|
||||||
|
export * from './utils'
|
||||||
|
|||||||
@@ -71,9 +71,9 @@ describe('lintText', () => {
|
|||||||
|
|
||||||
describe('lintFile', () => {
|
describe('lintFile', () => {
|
||||||
it('should identify lint issues in a given file', async () => {
|
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({
|
expect(results).toContainEqual({
|
||||||
message: 'Line contains trailing spaces',
|
message: 'Line contains trailing spaces',
|
||||||
lineNumber: 1,
|
lineNumber: 1,
|
||||||
@@ -95,6 +95,13 @@ describe('lintFile', () => {
|
|||||||
endColumnNumber: 1,
|
endColumnNumber: 1,
|
||||||
severity: Severity.Warning
|
severity: Severity.Warning
|
||||||
})
|
})
|
||||||
|
expect(results).toContainEqual({
|
||||||
|
message: 'File name contains uppercase characters',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
expect(results).toContainEqual({
|
expect(results).toContainEqual({
|
||||||
message: 'File missing Doxygen header',
|
message: 'File missing Doxygen header',
|
||||||
lineNumber: 1,
|
lineNumber: 1,
|
||||||
@@ -105,10 +112,24 @@ describe('lintFile', () => {
|
|||||||
expect(results).toContainEqual({
|
expect(results).toContainEqual({
|
||||||
message: 'Line contains encoded password',
|
message: 'Line contains encoded password',
|
||||||
lineNumber: 5,
|
lineNumber: 5,
|
||||||
startColumnNumber: 11,
|
startColumnNumber: 10,
|
||||||
endColumnNumber: 19,
|
endColumnNumber: 18,
|
||||||
severity: Severity.Error
|
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
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const processLine = (
|
|||||||
): Diagnostic[] => {
|
): Diagnostic[] => {
|
||||||
const diagnostics: Diagnostic[] = []
|
const diagnostics: Diagnostic[] = []
|
||||||
config.lineLintRules.forEach((rule) => {
|
config.lineLintRules.forEach((rule) => {
|
||||||
diagnostics.push(...rule.test(line, lineNumber))
|
diagnostics.push(...rule.test(line, lineNumber, config))
|
||||||
})
|
})
|
||||||
|
|
||||||
return diagnostics
|
return diagnostics
|
||||||
|
|||||||
@@ -34,6 +34,27 @@ describe('hasDoxygenHeader', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should return an array with a single diagnostic when the file has comment blocks but no header', () => {
|
||||||
|
const content = `
|
||||||
|
%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
|
||||||
|
%local x libref;
|
||||||
|
%let x={SAS002};
|
||||||
|
/** Comment Line 1
|
||||||
|
* Comment Line 2
|
||||||
|
*/
|
||||||
|
%do x=0 %to &maxtries;`
|
||||||
|
|
||||||
|
expect(hasDoxygenHeader.test(content)).toEqual([
|
||||||
|
{
|
||||||
|
message: 'File missing Doxygen header',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it('should return an array with a single diagnostic when the file is undefined', () => {
|
it('should return an array with a single diagnostic when the file is undefined', () => {
|
||||||
const content = undefined
|
const content = undefined
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const description =
|
|||||||
const message = 'File missing Doxygen header'
|
const message = 'File missing Doxygen header'
|
||||||
const test = (value: string) => {
|
const test = (value: string) => {
|
||||||
try {
|
try {
|
||||||
const hasFileHeader = value.split('/**')[0] !== value
|
const hasFileHeader = value.trimStart().startsWith('/*')
|
||||||
if (hasFileHeader) return []
|
if (hasFileHeader) return []
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
74
src/rules/indentationMultiple.spec.ts
Normal file
74
src/rules/indentationMultiple.spec.ts
Normal file
@@ -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
src/rules/indentationMultiple.ts
Normal file
41
src/rules/indentationMultiple.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
27
src/rules/lowerCaseFileNames.spec.ts
Normal file
27
src/rules/lowerCaseFileNames.spec.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
32
src/rules/lowerCaseFileNames.ts
Normal file
32
src/rules/lowerCaseFileNames.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
44
src/rules/maxLineLength.spec.ts
Normal file
44
src/rules/maxLineLength.spec.ts
Normal file
@@ -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([])
|
||||||
|
})
|
||||||
|
})
|
||||||
32
src/rules/maxLineLength.ts
Normal file
32
src/rules/maxLineLength.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PathLintRule } from '../types/LintRule'
|
import { PathLintRule } from '../types/LintRule'
|
||||||
import { LintRuleType } from '../types/LintRuleType'
|
import { LintRuleType } from '../types/LintRuleType'
|
||||||
import path from 'path'
|
|
||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../types/Severity'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
const name = 'noSpacesInFileNames'
|
const name = 'noSpacesInFileNames'
|
||||||
const description = 'Enforce the absence of spaces within file names.'
|
const description = 'Enforce the absence of spaces within file names.'
|
||||||
|
|||||||
22
src/rules/noTabIndentation.spec.ts
Normal file
22
src/rules/noTabIndentation.spec.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
30
src/rules/noTabIndentation.ts
Normal file
30
src/rules/noTabIndentation.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -40,6 +40,20 @@ describe('LintConfig', () => {
|
|||||||
expect(config.fileLintRules[0].type).toEqual(LintRuleType.File)
|
expect(config.fileLintRules[0].type).toEqual(LintRuleType.File)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should create an instance with the indentation multiple set', () => {
|
||||||
|
const config = new LintConfig({ indentationMultiple: 5 })
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.indentationMultiple).toEqual(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create an instance with the indentation multiple turned off', () => {
|
||||||
|
const config = new LintConfig({ indentationMultiple: 0 })
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.indentationMultiple).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
it('should create an instance with all flags set', () => {
|
it('should create an instance with all flags set', () => {
|
||||||
const config = new LintConfig({
|
const config = new LintConfig({
|
||||||
noTrailingSpaces: true,
|
noTrailingSpaces: true,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { hasDoxygenHeader } from '../rules/hasDoxygenHeader'
|
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'
|
import { noEncodedPasswords } from '../rules/noEncodedPasswords'
|
||||||
import { noSpacesInFileNames } from '../rules/noSpacesInFileNames'
|
import { noSpacesInFileNames } from '../rules/noSpacesInFileNames'
|
||||||
|
import { noTabIndentation } from '../rules/noTabIndentation'
|
||||||
import { noTrailingSpaces } from '../rules/noTrailingSpaces'
|
import { noTrailingSpaces } from '../rules/noTrailingSpaces'
|
||||||
import { FileLintRule, LineLintRule, PathLintRule } from './LintRule'
|
import { FileLintRule, LineLintRule, PathLintRule } from './LintRule'
|
||||||
|
|
||||||
@@ -15,6 +19,8 @@ export class LintConfig {
|
|||||||
readonly lineLintRules: LineLintRule[] = []
|
readonly lineLintRules: LineLintRule[] = []
|
||||||
readonly fileLintRules: FileLintRule[] = []
|
readonly fileLintRules: FileLintRule[] = []
|
||||||
readonly pathLintRules: PathLintRule[] = []
|
readonly pathLintRules: PathLintRule[] = []
|
||||||
|
readonly maxLineLength: number = 80
|
||||||
|
readonly indentationMultiple: number = 2
|
||||||
|
|
||||||
constructor(json?: any) {
|
constructor(json?: any) {
|
||||||
if (json?.noTrailingSpaces) {
|
if (json?.noTrailingSpaces) {
|
||||||
@@ -25,6 +31,20 @@ export class LintConfig {
|
|||||||
this.lineLintRules.push(noEncodedPasswords)
|
this.lineLintRules.push(noEncodedPasswords)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (json?.noTabIndentation) {
|
||||||
|
this.lineLintRules.push(noTabIndentation)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json?.maxLineLength) {
|
||||||
|
this.maxLineLength = json.maxLineLength
|
||||||
|
this.lineLintRules.push(maxLineLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNaN(json?.indentationMultiple)) {
|
||||||
|
this.indentationMultiple = json.indentationMultiple as number
|
||||||
|
this.lineLintRules.push(indentationMultiple)
|
||||||
|
}
|
||||||
|
|
||||||
if (json?.hasDoxygenHeader) {
|
if (json?.hasDoxygenHeader) {
|
||||||
this.fileLintRules.push(hasDoxygenHeader)
|
this.fileLintRules.push(hasDoxygenHeader)
|
||||||
}
|
}
|
||||||
@@ -32,5 +52,9 @@ export class LintConfig {
|
|||||||
if (json?.noSpacesInFileNames) {
|
if (json?.noSpacesInFileNames) {
|
||||||
this.pathLintRules.push(noSpacesInFileNames)
|
this.pathLintRules.push(noSpacesInFileNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (json?.lowerCaseFileNames) {
|
||||||
|
this.pathLintRules.push(lowerCaseFileNames)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Diagnostic } from './Diagnostic'
|
import { Diagnostic } from './Diagnostic'
|
||||||
|
import { LintConfig } from './LintConfig'
|
||||||
import { LintRuleType } from './LintRuleType'
|
import { LintRuleType } from './LintRuleType'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,7 +18,7 @@ export interface LintRule {
|
|||||||
*/
|
*/
|
||||||
export interface LineLintRule extends LintRule {
|
export interface LineLintRule extends LintRule {
|
||||||
type: LintRuleType.Line
|
type: LintRuleType.Line
|
||||||
test: (value: string, lineNumber: number) => Diagnostic[]
|
test: (value: string, lineNumber: number, config?: LintConfig) => Diagnostic[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ describe('getLintConfig', () => {
|
|||||||
|
|
||||||
expect(config).toBeInstanceOf(LintConfig)
|
expect(config).toBeInstanceOf(LintConfig)
|
||||||
expect(config.fileLintRules.length).toEqual(1)
|
expect(config.fileLintRules.length).toEqual(1)
|
||||||
expect(config.lineLintRules.length).toEqual(2)
|
expect(config.lineLintRules.length).toEqual(5)
|
||||||
|
expect(config.pathLintRules.length).toEqual(2)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,11 +3,20 @@ import { LintConfig } from '../types/LintConfig'
|
|||||||
import { readFile } from '@sasjs/utils/file'
|
import { readFile } from '@sasjs/utils/file'
|
||||||
import { getProjectRoot } from './getProjectRoot'
|
import { getProjectRoot } from './getProjectRoot'
|
||||||
|
|
||||||
const defaultConfiguration = {
|
/**
|
||||||
|
* Default configuration that is used when a .sasjslint file is not found
|
||||||
|
*/
|
||||||
|
export const DefaultLintConfiguration = {
|
||||||
noTrailingSpaces: true,
|
noTrailingSpaces: true,
|
||||||
noEncodedPasswords: true,
|
noEncodedPasswords: true,
|
||||||
hasDoxygenHeader: true
|
hasDoxygenHeader: true,
|
||||||
|
noSpacesInFileNames: true,
|
||||||
|
lowerCaseFileNames: true,
|
||||||
|
maxLineLength: 80,
|
||||||
|
noTabIndentation: true,
|
||||||
|
indentationMultiple: 2
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the config from the .sasjslint file and creates a LintConfig object.
|
* Fetches the config from the .sasjslint file and creates a LintConfig object.
|
||||||
* Returns the default configuration when a .sasjslint file is unavailable.
|
* Returns the default configuration when a .sasjslint file is unavailable.
|
||||||
@@ -18,8 +27,7 @@ export async function getLintConfig(): Promise<LintConfig> {
|
|||||||
const configuration = await readFile(
|
const configuration = await readFile(
|
||||||
path.join(projectRoot, '.sasjslint')
|
path.join(projectRoot, '.sasjslint')
|
||||||
).catch((_) => {
|
).catch((_) => {
|
||||||
console.warn('Unable to load .sasjslint file. Using default configuration.')
|
return JSON.stringify(DefaultLintConfiguration)
|
||||||
return JSON.stringify(defaultConfiguration)
|
|
||||||
})
|
})
|
||||||
return new LintConfig(JSON.parse(configuration))
|
return new LintConfig(JSON.parse(configuration))
|
||||||
}
|
}
|
||||||
|
|||||||
2
src/utils/index.ts
Normal file
2
src/utils/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './getLintConfig'
|
||||||
|
export * from './getProjectRoot'
|
||||||
Reference in New Issue
Block a user