1
0
mirror of https://github.com/sasjs/lint.git synced 2025-12-12 02:14:35 +00:00

Compare commits

...

2 Commits

Author SHA1 Message Date
Krishna Acondy
f10e6e5378 Merge pull request #6 from sasjs/add-severity-column-numbers
fix(*): Add severity, start and end column numbers for diagnostics
2021-03-24 09:21:08 +00:00
Krishna Acondy
de1fabc394 fix(*): Add severity, start and end column numbers for diagnostics, change warning to message 2021-03-24 09:11:09 +00:00
12 changed files with 126 additions and 37 deletions

View File

@@ -1,4 +1,5 @@
import { lint, splitText } from './lint' import { lint, splitText } from './lint'
import { Severity } from './types/Severity'
describe('lint', () => { describe('lint', () => {
it('should identify trailing spaces', async () => { it('should identify trailing spaces', async () => {
@@ -11,14 +12,18 @@ describe('lint', () => {
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
}) })
}) })
@@ -31,9 +36,11 @@ describe('lint', () => {
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
}) })
}) })
@@ -43,9 +50,11 @@ describe('lint', () => {
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
}) })
}) })

View File

@@ -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
}
]) ])
}) })
}) })

View File

@@ -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
} }

View File

@@ -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
} }
]) ])
}) })

View File

@@ -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
} }

View File

@@ -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
} }
]) ])
}) })

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -2,14 +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[] test: (value: string, lineNumber: number) => Diagnostic[]
} }

8
src/types/Severity.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* Severity indicates the seriousness of a given violation.
*/
export enum Severity {
Info,
Warning,
Error
}

View File

@@ -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)
})
}) })

View File

@@ -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))