mirror of
https://github.com/sasjs/lint.git
synced 2025-12-15 11:24:36 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1be358ca51 | ||
|
|
6c09745cc6 | ||
|
|
c92630a8f9 | ||
|
|
c88aa8b3f6 | ||
|
|
f10e6e5378 | ||
|
|
de1fabc394 |
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"noTrailingSpaces": true,
|
"noTrailingSpaces": true,
|
||||||
"noEncodedPasswords": true,
|
"noEncodedPasswords": true,
|
||||||
"hasDoxygenHeader": true
|
"hasDoxygenHeader": true,
|
||||||
|
"noSpacesInFileNames": true
|
||||||
}
|
}
|
||||||
55
sasjslint-schema.json
Normal file
55
sasjslint-schema.json
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://github.com/sasjs/lint/blob/main/sasjslint-schema.json",
|
||||||
|
"type": "object",
|
||||||
|
"title": "SASjs Lint Config File",
|
||||||
|
"description": "The SASjs Lint Config file provides the settings for customising SAS code style in your project.",
|
||||||
|
"default": {
|
||||||
|
"noTrailingSpaces": true,
|
||||||
|
"noEncodedPasswords": true,
|
||||||
|
"hasDoxygenHeader": true,
|
||||||
|
"noSpacesInFileNames": true
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"noTrailingSpaces": true,
|
||||||
|
"noEncodedPasswords": true,
|
||||||
|
"hasDoxygenHeader": true,
|
||||||
|
"noSpacesInFileNames": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"noTrailingSpaces": {
|
||||||
|
"$id": "#/properties/noTrailingSpaces",
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "noTrailingSpaces",
|
||||||
|
"description": "Enforces no trailing spaces in lines of SAS code. Shows a warning when they are present.",
|
||||||
|
"default": true,
|
||||||
|
"examples": [true, false]
|
||||||
|
},
|
||||||
|
"noEncodedPasswords": {
|
||||||
|
"$id": "#/properties/noEncodedPasswords",
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "noEncodedPasswords",
|
||||||
|
"description": "Enforces no encoded passwords such as {SAS001} or {SASENC} in lines of SAS code. Shows an error when they are present.",
|
||||||
|
"default": true,
|
||||||
|
"examples": [true, false]
|
||||||
|
},
|
||||||
|
"hasDoxygenHeader": {
|
||||||
|
"$id": "#/properties/hasDoxygenHeader",
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "hasDoxygenHeader",
|
||||||
|
"description": "Enforces the presence of a Doxygen header in the form of a comment block at the start of each SAS file. Shows a warning when one is absent.",
|
||||||
|
"default": true,
|
||||||
|
"examples": [true, false]
|
||||||
|
},
|
||||||
|
"noSpacesInFileNames": {
|
||||||
|
"$id": "#/properties/noSpacesInFileNames",
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "noSpacesInFileNames",
|
||||||
|
"description": "Enforces no spaces in file names. Shows a warning when they are present.",
|
||||||
|
"default": true,
|
||||||
|
"examples": [true, false]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/example file.sas
Normal file
17
src/example file.sas
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
%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,4 +1,4 @@
|
|||||||
import { lint } from './lint'
|
import { lintText } from './lint'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example which tests a piece of text with all known violations.
|
* Example which tests a piece of text with all known violations.
|
||||||
@@ -46,4 +46,4 @@ const text = `/**
|
|||||||
%mend;
|
%mend;
|
||||||
`
|
`
|
||||||
|
|
||||||
lint(text).then((diagnostics) => console.table(diagnostics))
|
lintText(text).then((diagnostics) => console.table(diagnostics))
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export { lint } from './lint'
|
export { lintText, lintFile } from './lint'
|
||||||
|
export * from './types'
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
import { lint, splitText } from './lint'
|
import { lintFile, lintText, splitText } from './lint'
|
||||||
|
import { Severity } from './types/Severity'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
describe('lint', () => {
|
describe('lintText', () => {
|
||||||
it('should identify trailing spaces', async () => {
|
it('should identify trailing spaces', async () => {
|
||||||
const text = `/**
|
const text = `/**
|
||||||
@file
|
@file
|
||||||
**/
|
**/
|
||||||
%put 'hello';
|
%put 'hello';
|
||||||
%put 'world'; `
|
%put 'world'; `
|
||||||
const results = await lint(text)
|
const results = await lintText(text)
|
||||||
|
|
||||||
expect(results.length).toEqual(2)
|
expect(results.length).toEqual(2)
|
||||||
expect(results[0]).toEqual({
|
expect(results[0]).toEqual({
|
||||||
warning: 'Line contains trailing spaces',
|
message: 'Line contains trailing spaces',
|
||||||
lineNumber: 4,
|
lineNumber: 4,
|
||||||
columnNumber: 18
|
startColumnNumber: 18,
|
||||||
|
endColumnNumber: 18,
|
||||||
|
severity: Severity.Warning
|
||||||
})
|
})
|
||||||
expect(results[1]).toEqual({
|
expect(results[1]).toEqual({
|
||||||
warning: 'Line contains trailing spaces',
|
message: 'Line contains trailing spaces',
|
||||||
lineNumber: 5,
|
lineNumber: 5,
|
||||||
columnNumber: 22
|
startColumnNumber: 22,
|
||||||
|
endColumnNumber: 23,
|
||||||
|
severity: Severity.Warning
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -27,25 +33,29 @@ describe('lint', () => {
|
|||||||
@file
|
@file
|
||||||
**/
|
**/
|
||||||
%put '{SAS001}';`
|
%put '{SAS001}';`
|
||||||
const results = await lint(text)
|
const results = await lintText(text)
|
||||||
|
|
||||||
expect(results.length).toEqual(1)
|
expect(results.length).toEqual(1)
|
||||||
expect(results[0]).toEqual({
|
expect(results[0]).toEqual({
|
||||||
warning: 'Line contains encoded password',
|
message: 'Line contains encoded password',
|
||||||
lineNumber: 4,
|
lineNumber: 4,
|
||||||
columnNumber: 11
|
startColumnNumber: 11,
|
||||||
|
endColumnNumber: 19,
|
||||||
|
severity: Severity.Error
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should identify missing doxygen header', async () => {
|
it('should identify missing doxygen header', async () => {
|
||||||
const text = `%put 'hello';`
|
const text = `%put 'hello';`
|
||||||
const results = await lint(text)
|
const results = await lintText(text)
|
||||||
|
|
||||||
expect(results.length).toEqual(1)
|
expect(results.length).toEqual(1)
|
||||||
expect(results[0]).toEqual({
|
expect(results[0]).toEqual({
|
||||||
warning: 'File missing Doxygen header',
|
message: 'File missing Doxygen header',
|
||||||
lineNumber: 1,
|
lineNumber: 1,
|
||||||
columnNumber: 1
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -53,12 +63,55 @@ describe('lint', () => {
|
|||||||
const text = `/**
|
const text = `/**
|
||||||
@file
|
@file
|
||||||
**/`
|
**/`
|
||||||
const results = await lint(text)
|
const results = await lintText(text)
|
||||||
|
|
||||||
expect(results.length).toEqual(0)
|
expect(results.length).toEqual(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('lintFile', () => {
|
||||||
|
it('should identify lint issues in a given file', async () => {
|
||||||
|
const results = await lintFile(path.join(__dirname, 'example file.sas'))
|
||||||
|
|
||||||
|
expect(results.length).toEqual(5)
|
||||||
|
expect(results).toContainEqual({
|
||||||
|
message: 'Line contains trailing spaces',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 2,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(results).toContainEqual({
|
||||||
|
message: 'Line contains trailing spaces',
|
||||||
|
lineNumber: 2,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 2,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(results).toContainEqual({
|
||||||
|
message: 'File name contains spaces',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(results).toContainEqual({
|
||||||
|
message: 'File missing Doxygen header',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(results).toContainEqual({
|
||||||
|
message: 'Line contains encoded password',
|
||||||
|
lineNumber: 5,
|
||||||
|
startColumnNumber: 11,
|
||||||
|
endColumnNumber: 19,
|
||||||
|
severity: Severity.Error
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('splitText', () => {
|
describe('splitText', () => {
|
||||||
it('should return an empty array when text is falsy', () => {
|
it('should return an empty array when text is falsy', () => {
|
||||||
const lines = splitText('')
|
const lines = splitText('')
|
||||||
|
|||||||
33
src/lint.ts
33
src/lint.ts
@@ -1,3 +1,4 @@
|
|||||||
|
import { readFile } from '@sasjs/utils/file'
|
||||||
import { Diagnostic } from './types/Diagnostic'
|
import { Diagnostic } from './types/Diagnostic'
|
||||||
import { LintConfig } from './types/LintConfig'
|
import { LintConfig } from './types/LintConfig'
|
||||||
import { getLintConfig } from './utils/getLintConfig'
|
import { getLintConfig } from './utils/getLintConfig'
|
||||||
@@ -7,11 +8,26 @@ import { getLintConfig } from './utils/getLintConfig'
|
|||||||
* @param {string} text - the text content to be linted.
|
* @param {string} text - the text content to be linted.
|
||||||
* @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number.
|
* @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number.
|
||||||
*/
|
*/
|
||||||
export const lint = async (text: string) => {
|
export const lintText = async (text: string) => {
|
||||||
const config = await getLintConfig()
|
const config = await getLintConfig()
|
||||||
return processText(text, config)
|
return processText(text, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyses and produces a set of diagnostics for the file at the given path.
|
||||||
|
* @param {string} filePath - the path to the file to be linted.
|
||||||
|
* @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number.
|
||||||
|
*/
|
||||||
|
export const lintFile = async (filePath: string) => {
|
||||||
|
const config = await getLintConfig()
|
||||||
|
const text = await readFile(filePath)
|
||||||
|
|
||||||
|
const fileDiagnostics = processFile(filePath, config)
|
||||||
|
const textDiagnostics = processText(text, config)
|
||||||
|
|
||||||
|
return [...fileDiagnostics, ...textDiagnostics]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Splits the given content into a list of lines, regardless of CRLF or LF line endings.
|
* Splits the given content into a list of lines, regardless of CRLF or LF line endings.
|
||||||
* @param {string} text - the text content to be split into lines.
|
* @param {string} text - the text content to be split into lines.
|
||||||
@@ -25,7 +41,7 @@ export const splitText = (text: string): string[] => {
|
|||||||
const processText = (text: string, config: LintConfig) => {
|
const processText = (text: string, config: LintConfig) => {
|
||||||
const lines = splitText(text)
|
const lines = splitText(text)
|
||||||
const diagnostics: Diagnostic[] = []
|
const diagnostics: Diagnostic[] = []
|
||||||
diagnostics.push(...processFile(config, text))
|
diagnostics.push(...processContent(config, text))
|
||||||
lines.forEach((line, index) => {
|
lines.forEach((line, index) => {
|
||||||
diagnostics.push(...processLine(config, line, index + 1))
|
diagnostics.push(...processLine(config, line, index + 1))
|
||||||
})
|
})
|
||||||
@@ -33,10 +49,10 @@ const processText = (text: string, config: LintConfig) => {
|
|||||||
return diagnostics
|
return diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
const processFile = (config: LintConfig, fileContent: string): Diagnostic[] => {
|
const processContent = (config: LintConfig, content: string): Diagnostic[] => {
|
||||||
const diagnostics: Diagnostic[] = []
|
const diagnostics: Diagnostic[] = []
|
||||||
config.fileLintRules.forEach((rule) => {
|
config.fileLintRules.forEach((rule) => {
|
||||||
diagnostics.push(...rule.test(fileContent))
|
diagnostics.push(...rule.test(content))
|
||||||
})
|
})
|
||||||
|
|
||||||
return diagnostics
|
return diagnostics
|
||||||
@@ -54,3 +70,12 @@ const processLine = (
|
|||||||
|
|
||||||
return diagnostics
|
return diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const processFile = (filePath: string, config: LintConfig): Diagnostic[] => {
|
||||||
|
const diagnostics: Diagnostic[] = []
|
||||||
|
config.pathLintRules.forEach((rule) => {
|
||||||
|
diagnostics.push(...rule.test(filePath))
|
||||||
|
})
|
||||||
|
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Severity } from '../types/Severity'
|
||||||
import { hasDoxygenHeader } from './hasDoxygenHeader'
|
import { hasDoxygenHeader } from './hasDoxygenHeader'
|
||||||
|
|
||||||
describe('hasDoxygenHeader', () => {
|
describe('hasDoxygenHeader', () => {
|
||||||
@@ -23,7 +24,13 @@ describe('hasDoxygenHeader', () => {
|
|||||||
%do x=0 %to &maxtries;`
|
%do x=0 %to &maxtries;`
|
||||||
|
|
||||||
expect(hasDoxygenHeader.test(content)).toEqual([
|
expect(hasDoxygenHeader.test(content)).toEqual([
|
||||||
{ warning: 'File missing Doxygen header', lineNumber: 1, columnNumber: 1 }
|
{
|
||||||
|
message: 'File missing Doxygen header',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -31,7 +38,13 @@ describe('hasDoxygenHeader', () => {
|
|||||||
const content = undefined
|
const content = undefined
|
||||||
|
|
||||||
expect(hasDoxygenHeader.test((content as unknown) as string)).toEqual([
|
expect(hasDoxygenHeader.test((content as unknown) as string)).toEqual([
|
||||||
{ warning: 'File missing Doxygen header', lineNumber: 1, columnNumber: 1 }
|
{
|
||||||
|
message: 'File missing Doxygen header',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,17 +1,34 @@
|
|||||||
import { FileLintRule } from '../types/LintRule'
|
import { FileLintRule } from '../types/LintRule'
|
||||||
import { LintRuleType } from '../types/LintRuleType'
|
import { LintRuleType } from '../types/LintRuleType'
|
||||||
|
import { Severity } from '../types/Severity'
|
||||||
|
|
||||||
const name = 'hasDoxygenHeader'
|
const name = 'hasDoxygenHeader'
|
||||||
const description =
|
const description =
|
||||||
'Enforce the presence of a Doxygen header at the start of each file.'
|
'Enforce the presence of a Doxygen header at the start of each file.'
|
||||||
const warning = '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.split('/**')[0] !== value
|
||||||
if (hasFileHeader) return []
|
if (hasFileHeader) return []
|
||||||
return [{ warning, lineNumber: 1, columnNumber: 1 }]
|
return [
|
||||||
|
{
|
||||||
|
message,
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
]
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return [{ warning, lineNumber: 1, columnNumber: 1 }]
|
return [
|
||||||
|
{
|
||||||
|
message,
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +39,6 @@ export const hasDoxygenHeader: FileLintRule = {
|
|||||||
type: LintRuleType.File,
|
type: LintRuleType.File,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
warning,
|
message,
|
||||||
test
|
test
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Severity } from '../types/Severity'
|
||||||
import { noEncodedPasswords } from './noEncodedPasswords'
|
import { noEncodedPasswords } from './noEncodedPasswords'
|
||||||
|
|
||||||
describe('noEncodedPasswords', () => {
|
describe('noEncodedPasswords', () => {
|
||||||
@@ -10,9 +11,11 @@ describe('noEncodedPasswords', () => {
|
|||||||
const line = "%put '{SASENC}'; "
|
const line = "%put '{SASENC}'; "
|
||||||
expect(noEncodedPasswords.test(line, 1)).toEqual([
|
expect(noEncodedPasswords.test(line, 1)).toEqual([
|
||||||
{
|
{
|
||||||
warning: 'Line contains encoded password',
|
message: 'Line contains encoded password',
|
||||||
lineNumber: 1,
|
lineNumber: 1,
|
||||||
columnNumber: 7
|
startColumnNumber: 7,
|
||||||
|
endColumnNumber: 15,
|
||||||
|
severity: Severity.Error
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -21,9 +24,11 @@ describe('noEncodedPasswords', () => {
|
|||||||
const line = "%put '{SAS001}'; "
|
const line = "%put '{SAS001}'; "
|
||||||
expect(noEncodedPasswords.test(line, 1)).toEqual([
|
expect(noEncodedPasswords.test(line, 1)).toEqual([
|
||||||
{
|
{
|
||||||
warning: 'Line contains encoded password',
|
message: 'Line contains encoded password',
|
||||||
lineNumber: 1,
|
lineNumber: 1,
|
||||||
columnNumber: 7
|
startColumnNumber: 7,
|
||||||
|
endColumnNumber: 15,
|
||||||
|
severity: Severity.Error
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -32,14 +37,18 @@ describe('noEncodedPasswords', () => {
|
|||||||
const line = "%put '{SAS001} {SAS002}'; "
|
const line = "%put '{SAS001} {SAS002}'; "
|
||||||
expect(noEncodedPasswords.test(line, 1)).toEqual([
|
expect(noEncodedPasswords.test(line, 1)).toEqual([
|
||||||
{
|
{
|
||||||
warning: 'Line contains encoded password',
|
message: 'Line contains encoded password',
|
||||||
lineNumber: 1,
|
lineNumber: 1,
|
||||||
columnNumber: 7
|
startColumnNumber: 7,
|
||||||
|
endColumnNumber: 15,
|
||||||
|
severity: Severity.Error
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
warning: 'Line contains encoded password',
|
message: 'Line contains encoded password',
|
||||||
lineNumber: 1,
|
lineNumber: 1,
|
||||||
columnNumber: 16
|
startColumnNumber: 16,
|
||||||
|
endColumnNumber: 24,
|
||||||
|
severity: Severity.Error
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { LineLintRule } from '../types/LintRule'
|
import { LineLintRule } from '../types/LintRule'
|
||||||
import { LintRuleType } from '../types/LintRuleType'
|
import { LintRuleType } from '../types/LintRuleType'
|
||||||
|
import { Severity } from '../types/Severity'
|
||||||
|
|
||||||
const name = 'noEncodedPasswords'
|
const name = 'noEncodedPasswords'
|
||||||
const description = 'Disallow encoded passwords in SAS code.'
|
const description = 'Disallow encoded passwords in SAS code.'
|
||||||
const warning = 'Line contains encoded password'
|
const message = 'Line contains encoded password'
|
||||||
const test = (value: string, lineNumber: number) => {
|
const test = (value: string, lineNumber: number) => {
|
||||||
const regex = new RegExp(/{sas(\d{2,4}|enc)}[^;"'\s]*/, 'gi')
|
const regex = new RegExp(/{sas(\d{2,4}|enc)}[^;"'\s]*/, 'gi')
|
||||||
const matches = value.match(regex)
|
const matches = value.match(regex)
|
||||||
if (!matches || !matches.length) return []
|
if (!matches || !matches.length) return []
|
||||||
return matches.map((match) => ({
|
return matches.map((match) => ({
|
||||||
warning,
|
message,
|
||||||
lineNumber,
|
lineNumber,
|
||||||
columnNumber: value.indexOf(match) + 1
|
startColumnNumber: value.indexOf(match) + 1,
|
||||||
|
endColumnNumber: value.indexOf(match) + match.length + 1,
|
||||||
|
severity: Severity.Error
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +25,6 @@ export const noEncodedPasswords: LineLintRule = {
|
|||||||
type: LintRuleType.Line,
|
type: LintRuleType.Line,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
warning,
|
message,
|
||||||
test
|
test
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/rules/noSpacesInFileNames.spec.ts
Normal file
27
src/rules/noSpacesInFileNames.spec.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Severity } from '../types/Severity'
|
||||||
|
import { noSpacesInFileNames } from './noSpacesInFileNames'
|
||||||
|
|
||||||
|
describe('noSpacesInFileNames', () => {
|
||||||
|
it('should return an empty array when the file name has no spaces', () => {
|
||||||
|
const filePath = '/code/sas/my_sas_file.sas'
|
||||||
|
expect(noSpacesInFileNames.test(filePath)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an empty array when the file name has no spaces, even if the containing folder has spaces', () => {
|
||||||
|
const filePath = '/code/sas projects/my_sas_file.sas'
|
||||||
|
expect(noSpacesInFileNames.test(filePath)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a single diagnostic when the file name has spaces', () => {
|
||||||
|
const filePath = '/code/sas/my sas file.sas'
|
||||||
|
expect(noSpacesInFileNames.test(filePath)).toEqual([
|
||||||
|
{
|
||||||
|
message: 'File name contains spaces',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
34
src/rules/noSpacesInFileNames.ts
Normal file
34
src/rules/noSpacesInFileNames.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { PathLintRule } from '../types/LintRule'
|
||||||
|
import { LintRuleType } from '../types/LintRuleType'
|
||||||
|
import path from 'path'
|
||||||
|
import { Severity } from '../types/Severity'
|
||||||
|
|
||||||
|
const name = 'noSpacesInFileNames'
|
||||||
|
const description = 'Enforce the absence of spaces within file names.'
|
||||||
|
const message = 'File name contains spaces'
|
||||||
|
const test = (value: string) => {
|
||||||
|
const fileName = path.basename(value)
|
||||||
|
if (fileName.includes(' ')) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
message,
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lint rule that checks for the absence of spaces in a given file name.
|
||||||
|
*/
|
||||||
|
export const noSpacesInFileNames: PathLintRule = {
|
||||||
|
type: LintRuleType.Path,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
message,
|
||||||
|
test
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Severity } from '../types/Severity'
|
||||||
import { noTrailingSpaces } from './noTrailingSpaces'
|
import { noTrailingSpaces } from './noTrailingSpaces'
|
||||||
|
|
||||||
describe('noTrailingSpaces', () => {
|
describe('noTrailingSpaces', () => {
|
||||||
@@ -10,9 +11,11 @@ describe('noTrailingSpaces', () => {
|
|||||||
const line = "%put 'hello'; "
|
const line = "%put 'hello'; "
|
||||||
expect(noTrailingSpaces.test(line, 1)).toEqual([
|
expect(noTrailingSpaces.test(line, 1)).toEqual([
|
||||||
{
|
{
|
||||||
warning: 'Line contains trailing spaces',
|
message: 'Line contains trailing spaces',
|
||||||
lineNumber: 1,
|
lineNumber: 1,
|
||||||
columnNumber: 14
|
startColumnNumber: 14,
|
||||||
|
endColumnNumber: 15,
|
||||||
|
severity: Severity.Warning
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
import { LineLintRule } from '../types/LintRule'
|
import { LineLintRule } from '../types/LintRule'
|
||||||
import { LintRuleType } from '../types/LintRuleType'
|
import { LintRuleType } from '../types/LintRuleType'
|
||||||
|
import { Severity } from '../types/Severity'
|
||||||
|
|
||||||
const name = 'noTrailingSpaces'
|
const name = 'noTrailingSpaces'
|
||||||
const description = 'Disallow trailing spaces on lines.'
|
const description = 'Disallow trailing spaces on lines.'
|
||||||
const warning = 'Line contains trailing spaces'
|
const message = 'Line contains trailing spaces'
|
||||||
const test = (value: string, lineNumber: number) =>
|
const test = (value: string, lineNumber: number) =>
|
||||||
value.trimEnd() === value
|
value.trimEnd() === value
|
||||||
? []
|
? []
|
||||||
: [{ warning, lineNumber, columnNumber: value.trimEnd().length + 1 }]
|
: [
|
||||||
|
{
|
||||||
|
message,
|
||||||
|
lineNumber,
|
||||||
|
startColumnNumber: value.trimEnd().length + 1,
|
||||||
|
endColumnNumber: value.length,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lint rule that checks for the presence of trailing space(s) in a given line of text.
|
* Lint rule that checks for the presence of trailing space(s) in a given line of text.
|
||||||
@@ -16,6 +25,6 @@ export const noTrailingSpaces: LineLintRule = {
|
|||||||
type: LintRuleType.Line,
|
type: LintRuleType.Line,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
warning,
|
message,
|
||||||
test
|
test
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
import { Severity } from './Severity'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A diagnostic is produced by the execution of a lint rule against a file or line of text.
|
* A diagnostic is produced by the execution of a lint rule against a file or line of text.
|
||||||
*/
|
*/
|
||||||
export interface Diagnostic {
|
export interface Diagnostic {
|
||||||
lineNumber: number
|
lineNumber: number
|
||||||
columnNumber: number
|
startColumnNumber: number
|
||||||
warning: string
|
endColumnNumber: number
|
||||||
|
message: string
|
||||||
|
severity: Severity
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { hasDoxygenHeader } from '../rules/hasDoxygenHeader'
|
import { hasDoxygenHeader } from '../rules/hasDoxygenHeader'
|
||||||
import { noEncodedPasswords } from '../rules/noEncodedPasswords'
|
import { noEncodedPasswords } from '../rules/noEncodedPasswords'
|
||||||
|
import { noSpacesInFileNames } from '../rules/noSpacesInFileNames'
|
||||||
import { noTrailingSpaces } from '../rules/noTrailingSpaces'
|
import { noTrailingSpaces } from '../rules/noTrailingSpaces'
|
||||||
import { FileLintRule, LineLintRule } from './LintRule'
|
import { FileLintRule, LineLintRule, PathLintRule } from './LintRule'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LintConfig is the logical representation of the .sasjslint file.
|
* LintConfig is the logical representation of the .sasjslint file.
|
||||||
@@ -13,6 +14,7 @@ import { FileLintRule, LineLintRule } from './LintRule'
|
|||||||
export class LintConfig {
|
export class LintConfig {
|
||||||
readonly lineLintRules: LineLintRule[] = []
|
readonly lineLintRules: LineLintRule[] = []
|
||||||
readonly fileLintRules: FileLintRule[] = []
|
readonly fileLintRules: FileLintRule[] = []
|
||||||
|
readonly pathLintRules: PathLintRule[] = []
|
||||||
|
|
||||||
constructor(json?: any) {
|
constructor(json?: any) {
|
||||||
if (json?.noTrailingSpaces) {
|
if (json?.noTrailingSpaces) {
|
||||||
@@ -26,5 +28,9 @@ export class LintConfig {
|
|||||||
if (json?.hasDoxygenHeader) {
|
if (json?.hasDoxygenHeader) {
|
||||||
this.fileLintRules.push(hasDoxygenHeader)
|
this.fileLintRules.push(hasDoxygenHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (json?.noSpacesInFileNames) {
|
||||||
|
this.pathLintRules.push(noSpacesInFileNames)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ import { Diagnostic } from './Diagnostic'
|
|||||||
import { LintRuleType } from './LintRuleType'
|
import { LintRuleType } from './LintRuleType'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A lint rule is defined by a type, name, description, warning text and a test function.
|
* A lint rule is defined by a type, name, description, message text and a test function.
|
||||||
* The test function produces a set of diagnostics when executed.
|
* The test function produces a set of diagnostics when executed.
|
||||||
*/
|
*/
|
||||||
export interface LintRule {
|
export interface LintRule {
|
||||||
type: LintRuleType
|
type: LintRuleType
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
warning: string
|
message: string
|
||||||
test: (value: string, lineNumber: number) => Diagnostic[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,6 +17,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[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,3 +27,11 @@ export interface FileLintRule extends LintRule {
|
|||||||
type: LintRuleType.File
|
type: LintRuleType.File
|
||||||
test: (value: string) => Diagnostic[]
|
test: (value: string) => Diagnostic[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A PathLintRule is run once per file.
|
||||||
|
*/
|
||||||
|
export interface PathLintRule extends LintRule {
|
||||||
|
type: LintRuleType.Path
|
||||||
|
test: (value: string) => Diagnostic[]
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,5 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
export enum LintRuleType {
|
export enum LintRuleType {
|
||||||
Line,
|
Line,
|
||||||
File
|
File,
|
||||||
|
Path
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/types/Severity.ts
Normal file
8
src/types/Severity.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Severity indicates the seriousness of a given violation.
|
||||||
|
*/
|
||||||
|
export enum Severity {
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
Error
|
||||||
|
}
|
||||||
5
src/types/index.ts
Normal file
5
src/types/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './Diagnostic'
|
||||||
|
export * from './LintConfig'
|
||||||
|
export * from './LintRule'
|
||||||
|
export * from './LintRuleType'
|
||||||
|
export * from './Severity'
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as fileModule from '@sasjs/utils/file'
|
||||||
import { LintConfig } from '../types/LintConfig'
|
import { LintConfig } from '../types/LintConfig'
|
||||||
import { getLintConfig } from './getLintConfig'
|
import { getLintConfig } from './getLintConfig'
|
||||||
|
|
||||||
@@ -7,4 +8,16 @@ describe('getLintConfig', () => {
|
|||||||
|
|
||||||
expect(config).toBeInstanceOf(LintConfig)
|
expect(config).toBeInstanceOf(LintConfig)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should get the default config when a .sasjslint file is unavailable', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(fileModule, 'readFile')
|
||||||
|
.mockImplementationOnce(() => Promise.reject())
|
||||||
|
|
||||||
|
const config = await getLintConfig()
|
||||||
|
|
||||||
|
expect(config).toBeInstanceOf(LintConfig)
|
||||||
|
expect(config.fileLintRules.length).toEqual(1)
|
||||||
|
expect(config.lineLintRules.length).toEqual(2)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,14 +10,15 @@ const defaultConfiguration = {
|
|||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 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 {Promise<LintConfig>} resolves with an object representing the current lint configuration.
|
* @returns {Promise<LintConfig>} resolves with an object representing the current lint configuration.
|
||||||
*/
|
*/
|
||||||
export async function getLintConfig(): Promise<LintConfig> {
|
export async function getLintConfig(): Promise<LintConfig> {
|
||||||
const projectRoot = await getProjectRoot()
|
const projectRoot = await getProjectRoot()
|
||||||
const configuration = await readFile(
|
const configuration = await readFile(
|
||||||
path.join(projectRoot, '.sasjslint')
|
path.join(projectRoot, '.sasjslint')
|
||||||
).catch((e) => {
|
).catch((_) => {
|
||||||
console.error('Error reading .sasjslint file', e)
|
console.warn('Unable to load .sasjslint file. Using default configuration.')
|
||||||
return JSON.stringify(defaultConfiguration)
|
return JSON.stringify(defaultConfiguration)
|
||||||
})
|
})
|
||||||
return new LintConfig(JSON.parse(configuration))
|
return new LintConfig(JSON.parse(configuration))
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"**/*.spec.ts"
|
"**/*.spec.ts",
|
||||||
|
"**/example.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user