mirror of
https://github.com/sasjs/lint.git
synced 2025-12-11 09:54:35 +00:00
feat: new rules noNestedMacros & hasMacroParentheses
This commit is contained in:
@@ -7,5 +7,7 @@
|
|||||||
"lowerCaseFileNames": true,
|
"lowerCaseFileNames": true,
|
||||||
"noTabIndentation": true,
|
"noTabIndentation": true,
|
||||||
"indentationMultiple": 2,
|
"indentationMultiple": 2,
|
||||||
"hasMacroNameInMend": false
|
"hasMacroNameInMend": false,
|
||||||
|
"noNestedMacros": true,
|
||||||
|
"hasMacroParentheses": true
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ describe('hasMacroNameInMend', () => {
|
|||||||
message: 'mismatch macro name in %mend statement',
|
message: 'mismatch macro name in %mend statement',
|
||||||
lineNumber: 4,
|
lineNumber: 4,
|
||||||
startColumnNumber: 9,
|
startColumnNumber: 9,
|
||||||
endColumnNumber: 25,
|
endColumnNumber: 24,
|
||||||
severity: Severity.Warning
|
severity: Severity.Warning
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
@@ -219,7 +219,7 @@ describe('hasMacroNameInMend', () => {
|
|||||||
message: 'mismatch macro name in %mend statement',
|
message: 'mismatch macro name in %mend statement',
|
||||||
lineNumber: 6,
|
lineNumber: 6,
|
||||||
startColumnNumber: 14,
|
startColumnNumber: 14,
|
||||||
endColumnNumber: 30,
|
endColumnNumber: 29,
|
||||||
severity: Severity.Warning
|
severity: Severity.Warning
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { Diagnostic } from '../types/Diagnostic'
|
|||||||
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'
|
import { Severity } from '../types/Severity'
|
||||||
|
import { trimComments } from '../utils/trimComments'
|
||||||
|
import { getLineNumber } from '../utils/getLineNumber'
|
||||||
|
import { getColNumber } from '../utils/getColNumber'
|
||||||
|
|
||||||
const name = 'hasMacroNameInMend'
|
const name = 'hasMacroNameInMend'
|
||||||
const description = 'The %mend statement should contain the macro name'
|
const description = 'The %mend statement should contain the macro name'
|
||||||
@@ -46,7 +49,7 @@ const test = (value: string) => {
|
|||||||
lineNumber: getLineNumber(statements, index + 1),
|
lineNumber: getLineNumber(statements, index + 1),
|
||||||
startColumnNumber: getColNumber(statement, macroName),
|
startColumnNumber: getColNumber(statement, macroName),
|
||||||
endColumnNumber:
|
endColumnNumber:
|
||||||
getColNumber(statement, macroName) + macroName.length,
|
getColNumber(statement, macroName) + macroName.length - 1,
|
||||||
severity: Severity.Warning
|
severity: Severity.Warning
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -64,36 +67,6 @@ const test = (value: string) => {
|
|||||||
return diagnostics
|
return diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
const trimComments = (
|
|
||||||
statement: string,
|
|
||||||
commentStarted: boolean = false
|
|
||||||
): { statement: string; commentStarted: boolean } => {
|
|
||||||
let trimmed = statement.trim()
|
|
||||||
|
|
||||||
if (commentStarted || trimmed.startsWith('/*')) {
|
|
||||||
const parts = trimmed.split('*/')
|
|
||||||
if (parts.length > 1) {
|
|
||||||
return {
|
|
||||||
statement: (parts.pop() as string).trim(),
|
|
||||||
commentStarted: false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return { statement: '', commentStarted: true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { statement: trimmed, commentStarted: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLineNumber = (statements: string[], index: number): number => {
|
|
||||||
const combinedCode = statements.slice(0, index).join(';')
|
|
||||||
const lines = (combinedCode.match(/\n/g) || []).length + 1
|
|
||||||
return lines
|
|
||||||
}
|
|
||||||
|
|
||||||
const getColNumber = (statement: string, text: string): number => {
|
|
||||||
return (statement.split('\n').pop() as string).indexOf(text) + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lint rule that checks for the presence of macro name in %mend statement.
|
* Lint rule that checks for the presence of macro name in %mend statement.
|
||||||
*/
|
*/
|
||||||
|
|||||||
68
src/rules/hasMacroParentheses.ts
Normal file
68
src/rules/hasMacroParentheses.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { Diagnostic } from '../types/Diagnostic'
|
||||||
|
import { FileLintRule } from '../types/LintRule'
|
||||||
|
import { LintRuleType } from '../types/LintRuleType'
|
||||||
|
import { Severity } from '../types/Severity'
|
||||||
|
import { trimComments } from '../utils/trimComments'
|
||||||
|
import { getLineNumber } from '../utils/getLineNumber'
|
||||||
|
import { getColNumber } from '../utils/getColNumber'
|
||||||
|
|
||||||
|
const name = 'hasMacroParentheses'
|
||||||
|
const description = 'Macros are always defined with parentheses'
|
||||||
|
const message = 'Macro definition missing parentheses'
|
||||||
|
const test = (value: string) => {
|
||||||
|
const diagnostics: Diagnostic[] = []
|
||||||
|
|
||||||
|
const statements: string[] = value ? value.split(';') : []
|
||||||
|
|
||||||
|
let trimmedStatement = '',
|
||||||
|
commentStarted = false
|
||||||
|
statements.forEach((statement, index) => {
|
||||||
|
;({ statement: trimmedStatement, commentStarted } = trimComments(
|
||||||
|
statement,
|
||||||
|
commentStarted
|
||||||
|
))
|
||||||
|
|
||||||
|
if (trimmedStatement.startsWith('%macro ')) {
|
||||||
|
const macroNameDefinition = trimmedStatement
|
||||||
|
.slice(7, trimmedStatement.length)
|
||||||
|
.trim()
|
||||||
|
|
||||||
|
const macroNameDefinitionParts = macroNameDefinition.split('(')
|
||||||
|
const macroName = macroNameDefinitionParts[0]
|
||||||
|
if (macroNameDefinitionParts.length === 1)
|
||||||
|
diagnostics.push({
|
||||||
|
message,
|
||||||
|
lineNumber: getLineNumber(statements, index + 1),
|
||||||
|
startColumnNumber: getColNumber(statement, macroNameDefinition),
|
||||||
|
endColumnNumber:
|
||||||
|
getColNumber(statement, macroNameDefinition) +
|
||||||
|
macroNameDefinition.length -
|
||||||
|
1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
else if (macroName !== macroName.trim())
|
||||||
|
diagnostics.push({
|
||||||
|
message: 'Macro definition cannot have space',
|
||||||
|
lineNumber: getLineNumber(statements, index + 1),
|
||||||
|
startColumnNumber: getColNumber(statement, macroNameDefinition),
|
||||||
|
endColumnNumber:
|
||||||
|
getColNumber(statement, macroNameDefinition) +
|
||||||
|
macroNameDefinition.length -
|
||||||
|
1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lint rule that checks for the presence of macro name in %mend statement.
|
||||||
|
*/
|
||||||
|
export const hasMacroParentheses: FileLintRule = {
|
||||||
|
type: LintRuleType.File,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
message,
|
||||||
|
test
|
||||||
|
}
|
||||||
59
src/rules/noNestedMacros.ts
Normal file
59
src/rules/noNestedMacros.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Diagnostic } from '../types/Diagnostic'
|
||||||
|
import { FileLintRule } from '../types/LintRule'
|
||||||
|
import { LintRuleType } from '../types/LintRuleType'
|
||||||
|
import { Severity } from '../types/Severity'
|
||||||
|
import { trimComments } from '../utils/trimComments'
|
||||||
|
import { getLineNumber } from '../utils/getLineNumber'
|
||||||
|
import { getColNumber } from '../utils/getColNumber'
|
||||||
|
|
||||||
|
const name = 'noNestedMacros'
|
||||||
|
const description = 'Defining nested macro is not good practice'
|
||||||
|
const message = 'Macro definition present inside another macro'
|
||||||
|
const test = (value: string) => {
|
||||||
|
const diagnostics: Diagnostic[] = []
|
||||||
|
|
||||||
|
const statements: string[] = value ? value.split(';') : []
|
||||||
|
|
||||||
|
const stack: string[] = []
|
||||||
|
let trimmedStatement = '',
|
||||||
|
commentStarted = false
|
||||||
|
statements.forEach((statement, index) => {
|
||||||
|
;({ statement: trimmedStatement, commentStarted } = trimComments(
|
||||||
|
statement,
|
||||||
|
commentStarted
|
||||||
|
))
|
||||||
|
|
||||||
|
if (trimmedStatement.startsWith('%macro ')) {
|
||||||
|
const macroName = trimmedStatement
|
||||||
|
.split(' ')
|
||||||
|
.filter((s: string) => !!s)[1]
|
||||||
|
.split('(')[0]
|
||||||
|
if (stack.length) {
|
||||||
|
const parentMacro = stack.slice(-1).pop()
|
||||||
|
diagnostics.push({
|
||||||
|
message: `${message} '${parentMacro}'`,
|
||||||
|
lineNumber: getLineNumber(statements, index + 1),
|
||||||
|
startColumnNumber: getColNumber(statement, '%macro'),
|
||||||
|
endColumnNumber:
|
||||||
|
getColNumber(statement, '%macro') + trimmedStatement.length,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
}
|
||||||
|
stack.push(macroName)
|
||||||
|
} else if (trimmedStatement.startsWith('%mend')) {
|
||||||
|
stack.pop()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lint rule that checks for the presence of macro name in %mend statement.
|
||||||
|
*/
|
||||||
|
export const noNestedMacros: FileLintRule = {
|
||||||
|
type: LintRuleType.File,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
message,
|
||||||
|
test
|
||||||
|
}
|
||||||
@@ -58,6 +58,42 @@ describe('LintConfig', () => {
|
|||||||
expect(config.fileLintRules.length).toEqual(0)
|
expect(config.fileLintRules.length).toEqual(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should create an instance with the noNestedMacros flag set', () => {
|
||||||
|
const config = new LintConfig({ noNestedMacros: true })
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.lineLintRules.length).toEqual(0)
|
||||||
|
expect(config.fileLintRules.length).toEqual(1)
|
||||||
|
expect(config.fileLintRules[0].name).toEqual('noNestedMacros')
|
||||||
|
expect(config.fileLintRules[0].type).toEqual(LintRuleType.File)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create an instance with the noNestedMacros flag off', () => {
|
||||||
|
const config = new LintConfig({ noNestedMacros: false })
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.lineLintRules.length).toEqual(0)
|
||||||
|
expect(config.fileLintRules.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create an instance with the hasMacroParentheses flag set', () => {
|
||||||
|
const config = new LintConfig({ hasMacroParentheses: true })
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.lineLintRules.length).toEqual(0)
|
||||||
|
expect(config.fileLintRules.length).toEqual(1)
|
||||||
|
expect(config.fileLintRules[0].name).toEqual('hasMacroParentheses')
|
||||||
|
expect(config.fileLintRules[0].type).toEqual(LintRuleType.File)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create an instance with the hasMacroParentheses flag off', () => {
|
||||||
|
const config = new LintConfig({ hasMacroParentheses: false })
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.lineLintRules.length).toEqual(0)
|
||||||
|
expect(config.fileLintRules.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
it('should create an instance with the indentation multiple set', () => {
|
it('should create an instance with the indentation multiple set', () => {
|
||||||
const config = new LintConfig({ indentationMultiple: 5 })
|
const config = new LintConfig({ indentationMultiple: 5 })
|
||||||
|
|
||||||
@@ -82,7 +118,9 @@ describe('LintConfig', () => {
|
|||||||
maxLineLength: 80,
|
maxLineLength: 80,
|
||||||
noTabIndentation: true,
|
noTabIndentation: true,
|
||||||
indentationMultiple: 2,
|
indentationMultiple: 2,
|
||||||
hasMacroNameInMend: true
|
hasMacroNameInMend: true,
|
||||||
|
noNestedMacros: true,
|
||||||
|
hasMacroParentheses: true
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(config).toBeTruthy()
|
expect(config).toBeTruthy()
|
||||||
@@ -98,11 +136,15 @@ describe('LintConfig', () => {
|
|||||||
expect(config.lineLintRules[4].name).toEqual('indentationMultiple')
|
expect(config.lineLintRules[4].name).toEqual('indentationMultiple')
|
||||||
expect(config.lineLintRules[4].type).toEqual(LintRuleType.Line)
|
expect(config.lineLintRules[4].type).toEqual(LintRuleType.Line)
|
||||||
|
|
||||||
expect(config.fileLintRules.length).toEqual(2)
|
expect(config.fileLintRules.length).toEqual(4)
|
||||||
expect(config.fileLintRules[0].name).toEqual('hasDoxygenHeader')
|
expect(config.fileLintRules[0].name).toEqual('hasDoxygenHeader')
|
||||||
expect(config.fileLintRules[0].type).toEqual(LintRuleType.File)
|
expect(config.fileLintRules[0].type).toEqual(LintRuleType.File)
|
||||||
expect(config.fileLintRules[1].name).toEqual('hasMacroNameInMend')
|
expect(config.fileLintRules[1].name).toEqual('hasMacroNameInMend')
|
||||||
expect(config.fileLintRules[1].type).toEqual(LintRuleType.File)
|
expect(config.fileLintRules[1].type).toEqual(LintRuleType.File)
|
||||||
|
expect(config.fileLintRules[2].name).toEqual('noNestedMacros')
|
||||||
|
expect(config.fileLintRules[2].type).toEqual(LintRuleType.File)
|
||||||
|
expect(config.fileLintRules[3].name).toEqual('hasMacroParentheses')
|
||||||
|
expect(config.fileLintRules[3].type).toEqual(LintRuleType.File)
|
||||||
|
|
||||||
expect(config.pathLintRules.length).toEqual(2)
|
expect(config.pathLintRules.length).toEqual(2)
|
||||||
expect(config.pathLintRules[0].name).toEqual('noSpacesInFileNames')
|
expect(config.pathLintRules[0].name).toEqual('noSpacesInFileNames')
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { noSpacesInFileNames } from '../rules/noSpacesInFileNames'
|
|||||||
import { noTabIndentation } from '../rules/noTabIndentation'
|
import { noTabIndentation } from '../rules/noTabIndentation'
|
||||||
import { noTrailingSpaces } from '../rules/noTrailingSpaces'
|
import { noTrailingSpaces } from '../rules/noTrailingSpaces'
|
||||||
import { hasMacroNameInMend } from '../rules/hasMacroNameInMend'
|
import { hasMacroNameInMend } from '../rules/hasMacroNameInMend'
|
||||||
|
import { noNestedMacros } from '../rules/noNestedMacros'
|
||||||
|
import { hasMacroParentheses } from '../rules/hasMacroParentheses'
|
||||||
import { FileLintRule, LineLintRule, PathLintRule } from './LintRule'
|
import { FileLintRule, LineLintRule, PathLintRule } from './LintRule'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,5 +63,13 @@ export class LintConfig {
|
|||||||
if (json?.hasMacroNameInMend) {
|
if (json?.hasMacroNameInMend) {
|
||||||
this.fileLintRules.push(hasMacroNameInMend)
|
this.fileLintRules.push(hasMacroNameInMend)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (json?.noNestedMacros) {
|
||||||
|
this.fileLintRules.push(noNestedMacros)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json?.hasMacroParentheses) {
|
||||||
|
this.fileLintRules.push(hasMacroParentheses)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/utils/getColNumber.ts
Normal file
3
src/utils/getColNumber.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const getColNumber = (statement: string, text: string): number => {
|
||||||
|
return (statement.split('\n').pop() as string).indexOf(text) + 1
|
||||||
|
}
|
||||||
5
src/utils/getLineNumber.ts
Normal file
5
src/utils/getLineNumber.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const getLineNumber = (statements: string[], index: number): number => {
|
||||||
|
const combinedCode = statements.slice(0, index).join(';')
|
||||||
|
const lines = (combinedCode.match(/\n/g) || []).length + 1
|
||||||
|
return lines
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ describe('getLintConfig', () => {
|
|||||||
const config = await getLintConfig()
|
const config = await getLintConfig()
|
||||||
|
|
||||||
expect(config).toBeInstanceOf(LintConfig)
|
expect(config).toBeInstanceOf(LintConfig)
|
||||||
expect(config.fileLintRules.length).toEqual(1)
|
expect(config.fileLintRules.length).toEqual(3)
|
||||||
expect(config.lineLintRules.length).toEqual(5)
|
expect(config.lineLintRules.length).toEqual(5)
|
||||||
expect(config.pathLintRules.length).toEqual(2)
|
expect(config.pathLintRules.length).toEqual(2)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ export const DefaultLintConfiguration = {
|
|||||||
maxLineLength: 80,
|
maxLineLength: 80,
|
||||||
noTabIndentation: true,
|
noTabIndentation: true,
|
||||||
indentationMultiple: 2,
|
indentationMultiple: 2,
|
||||||
hasMacroNameInMend: false
|
hasMacroNameInMend: false,
|
||||||
|
noNestedMacros: true,
|
||||||
|
hasMacroParentheses: true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
19
src/utils/trimComments.ts
Normal file
19
src/utils/trimComments.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export const trimComments = (
|
||||||
|
statement: string,
|
||||||
|
commentStarted: boolean = false
|
||||||
|
): { statement: string; commentStarted: boolean } => {
|
||||||
|
let trimmed = statement.trim()
|
||||||
|
|
||||||
|
if (commentStarted || trimmed.startsWith('/*')) {
|
||||||
|
const parts = trimmed.split('*/')
|
||||||
|
if (parts.length > 1) {
|
||||||
|
return {
|
||||||
|
statement: (parts.pop() as string).trim(),
|
||||||
|
commentStarted: false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return { statement: '', commentStarted: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { statement: trimmed, commentStarted: false }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user