mirror of
https://github.com/sasjs/lint.git
synced 2025-12-12 02:14:35 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f10e6e5378 | ||
|
|
de1fabc394 |
@@ -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
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
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
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user