mirror of
https://github.com/sasjs/lint.git
synced 2026-01-15 08:10:05 +00:00
Merge pull request #21 from sasjs/issue-12
feat: new rule hasMacroNameInMend
This commit is contained in:
@@ -6,5 +6,6 @@
|
|||||||
"maxLineLength": 80,
|
"maxLineLength": 80,
|
||||||
"lowerCaseFileNames": true,
|
"lowerCaseFileNames": true,
|
||||||
"noTabIndentation": true,
|
"noTabIndentation": true,
|
||||||
"indentationMultiple": 2
|
"indentationMultiple": 2,
|
||||||
|
"hasMacroNameInMend": false
|
||||||
}
|
}
|
||||||
266
src/rules/hasMacroNameInMend.spec.ts
Normal file
266
src/rules/hasMacroNameInMend.spec.ts
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
import { Severity } from '../types/Severity'
|
||||||
|
import { hasMacroNameInMend } from './hasMacroNameInMend'
|
||||||
|
|
||||||
|
describe('hasMacroNameInMend', () => {
|
||||||
|
it('should return an empty array when %mend has correct macro name', () => {
|
||||||
|
const content = `
|
||||||
|
%macro somemacro();
|
||||||
|
%put &sysmacroname;
|
||||||
|
%mend somemacro;`
|
||||||
|
|
||||||
|
expect(hasMacroNameInMend.test(content)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an empty array when %mend has correct macro name without parentheses', () => {
|
||||||
|
const content = `
|
||||||
|
%macro somemacro;
|
||||||
|
%put &sysmacroname;
|
||||||
|
%mend somemacro;`
|
||||||
|
|
||||||
|
expect(hasMacroNameInMend.test(content)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a single diagnostic when %mend has no macro name', () => {
|
||||||
|
const content = `
|
||||||
|
%macro somemacro;
|
||||||
|
%put &sysmacroname;
|
||||||
|
%mend;`
|
||||||
|
|
||||||
|
expect(hasMacroNameInMend.test(content)).toEqual([
|
||||||
|
{
|
||||||
|
message: '%mend missing macro name',
|
||||||
|
lineNumber: 4,
|
||||||
|
startColumnNumber: 3,
|
||||||
|
endColumnNumber: 9,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a single diagnostic when %mend has incorrect macro name', () => {
|
||||||
|
const content = `
|
||||||
|
%macro somemacro;
|
||||||
|
%put &sysmacroname;
|
||||||
|
%mend someanothermacro;`
|
||||||
|
|
||||||
|
expect(hasMacroNameInMend.test(content)).toEqual([
|
||||||
|
{
|
||||||
|
message: 'mismatch macro name in %mend statement',
|
||||||
|
lineNumber: 4,
|
||||||
|
startColumnNumber: 9,
|
||||||
|
endColumnNumber: 25,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an empty array when the file is undefined', () => {
|
||||||
|
const content = undefined
|
||||||
|
|
||||||
|
expect(hasMacroNameInMend.test((content as unknown) as string)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('nestedMacros', () => {
|
||||||
|
it('should return an empty array when %mend has correct macro name', () => {
|
||||||
|
const content = `
|
||||||
|
%macro outer();
|
||||||
|
|
||||||
|
%macro inner();
|
||||||
|
%put inner;
|
||||||
|
%mend inner;
|
||||||
|
%inner()
|
||||||
|
%put outer;
|
||||||
|
%mend outer;`
|
||||||
|
|
||||||
|
expect(hasMacroNameInMend.test(content)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a single diagnostic when %mend has no macro name(inner)', () => {
|
||||||
|
const content = `
|
||||||
|
%macro outer();
|
||||||
|
|
||||||
|
%macro inner();
|
||||||
|
%put inner;
|
||||||
|
%mend;
|
||||||
|
%inner()
|
||||||
|
%put outer;
|
||||||
|
%mend outer;`
|
||||||
|
|
||||||
|
expect(hasMacroNameInMend.test(content)).toEqual([
|
||||||
|
{
|
||||||
|
message: '%mend missing macro name',
|
||||||
|
lineNumber: 6,
|
||||||
|
startColumnNumber: 5,
|
||||||
|
endColumnNumber: 11,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a single diagnostic when %mend has no macro name(outer)', () => {
|
||||||
|
const content = `
|
||||||
|
%macro outer();
|
||||||
|
|
||||||
|
%macro inner();
|
||||||
|
%put inner;
|
||||||
|
%mend inner;
|
||||||
|
%inner()
|
||||||
|
%put outer;
|
||||||
|
%mend;`
|
||||||
|
|
||||||
|
expect(hasMacroNameInMend.test(content)).toEqual([
|
||||||
|
{
|
||||||
|
message: '%mend missing macro name',
|
||||||
|
lineNumber: 9,
|
||||||
|
startColumnNumber: 3,
|
||||||
|
endColumnNumber: 9,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with two diagnostics when %mend has no macro name(none)', () => {
|
||||||
|
const content = `
|
||||||
|
%macro outer();
|
||||||
|
|
||||||
|
%macro inner();
|
||||||
|
%put inner;
|
||||||
|
%mend;
|
||||||
|
%inner()
|
||||||
|
%put outer;
|
||||||
|
%mend;`
|
||||||
|
|
||||||
|
expect(hasMacroNameInMend.test(content)).toEqual([
|
||||||
|
{
|
||||||
|
message: '%mend missing macro name',
|
||||||
|
lineNumber: 6,
|
||||||
|
startColumnNumber: 5,
|
||||||
|
endColumnNumber: 11,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: '%mend missing macro name',
|
||||||
|
lineNumber: 9,
|
||||||
|
startColumnNumber: 3,
|
||||||
|
endColumnNumber: 9,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with extra spaces and comments', () => {
|
||||||
|
it('should return an empty array when %mend has correct macro name', () => {
|
||||||
|
const content = `
|
||||||
|
/* 1st comment */
|
||||||
|
%macro somemacro ;
|
||||||
|
|
||||||
|
%put &sysmacroname;
|
||||||
|
|
||||||
|
/* 2nd
|
||||||
|
comment */
|
||||||
|
/* 3rd comment */ %mend somemacro ;`
|
||||||
|
|
||||||
|
expect(hasMacroNameInMend.test(content)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a single diagnostic when %mend has correct macro name having code in comments', () => {
|
||||||
|
const content = `/**
|
||||||
|
@file examplemacro.sas
|
||||||
|
@brief an example of a macro to be used in a service
|
||||||
|
@details This macro is great. Yadda yadda yadda. Usage:
|
||||||
|
|
||||||
|
* code formatting applies when indented by 4 spaces; code formatting applies when indented by 4 spaces; code formatting applies when indented by 4 spaces; code formatting applies when indented by 4 spaces; code formatting applies when indented by 4 spaces;
|
||||||
|
|
||||||
|
some code
|
||||||
|
%macro examplemacro123();
|
||||||
|
|
||||||
|
%examplemacro()
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li doesnothing.sas
|
||||||
|
|
||||||
|
@author Allan Bowe
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro examplemacro();
|
||||||
|
|
||||||
|
proc sql;
|
||||||
|
create table areas
|
||||||
|
as select area
|
||||||
|
|
||||||
|
from sashelp.springs;
|
||||||
|
|
||||||
|
%doesnothing();
|
||||||
|
|
||||||
|
%mend;`
|
||||||
|
|
||||||
|
expect(hasMacroNameInMend.test(content)).toEqual([
|
||||||
|
{
|
||||||
|
message: '%mend missing macro name',
|
||||||
|
lineNumber: 29,
|
||||||
|
startColumnNumber: 5,
|
||||||
|
endColumnNumber: 11,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a single diagnostic when %mend has incorrect macro name', () => {
|
||||||
|
const content = `
|
||||||
|
%macro somemacro;
|
||||||
|
/* some comments */
|
||||||
|
%put &sysmacroname;
|
||||||
|
/* some comments */
|
||||||
|
%mend someanothermacro ;`
|
||||||
|
|
||||||
|
expect(hasMacroNameInMend.test(content)).toEqual([
|
||||||
|
{
|
||||||
|
message: 'mismatch macro name in %mend statement',
|
||||||
|
lineNumber: 6,
|
||||||
|
startColumnNumber: 14,
|
||||||
|
endColumnNumber: 30,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a single diagnostic when %mend has no macro name', () => {
|
||||||
|
const content = `
|
||||||
|
%macro somemacro ;
|
||||||
|
/* some comments */%put &sysmacroname;
|
||||||
|
%mend ;`
|
||||||
|
|
||||||
|
expect(hasMacroNameInMend.test(content)).toEqual([
|
||||||
|
{
|
||||||
|
message: '%mend missing macro name',
|
||||||
|
lineNumber: 4,
|
||||||
|
startColumnNumber: 5,
|
||||||
|
endColumnNumber: 11,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('nestedMacros', () => {
|
||||||
|
it('should return an empty array when %mend has correct macro name', () => {
|
||||||
|
const content = `
|
||||||
|
%macro outer( ) ;
|
||||||
|
|
||||||
|
|
||||||
|
%macro inner();
|
||||||
|
|
||||||
|
%put inner;
|
||||||
|
|
||||||
|
%mend inner;
|
||||||
|
|
||||||
|
%inner()
|
||||||
|
|
||||||
|
%put outer;
|
||||||
|
%mend outer;`
|
||||||
|
|
||||||
|
expect(hasMacroNameInMend.test(content)).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
106
src/rules/hasMacroNameInMend.ts
Normal file
106
src/rules/hasMacroNameInMend.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { Diagnostic } from '../types/Diagnostic'
|
||||||
|
import { FileLintRule } from '../types/LintRule'
|
||||||
|
import { LintRuleType } from '../types/LintRuleType'
|
||||||
|
import { Severity } from '../types/Severity'
|
||||||
|
|
||||||
|
const name = 'hasMacroNameInMend'
|
||||||
|
const description = 'The %mend statement should contain the macro name'
|
||||||
|
const message = '$mend statement missing or incorrect'
|
||||||
|
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]
|
||||||
|
stack.push(macroName)
|
||||||
|
} else if (trimmedStatement.startsWith('%mend')) {
|
||||||
|
const macroStarted = stack.pop()
|
||||||
|
const macroName = trimmedStatement
|
||||||
|
.split(' ')
|
||||||
|
.filter((s: string) => !!s)[1]
|
||||||
|
|
||||||
|
if (!macroName) {
|
||||||
|
diagnostics.push({
|
||||||
|
message: '%mend missing macro name',
|
||||||
|
lineNumber: getLineNumber(statements, index + 1),
|
||||||
|
startColumnNumber: getColNumber(statement, '%mend'),
|
||||||
|
endColumnNumber: getColNumber(statement, '%mend') + 6,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
} else if (macroName !== macroStarted) {
|
||||||
|
diagnostics.push({
|
||||||
|
message: 'mismatch macro name in %mend statement',
|
||||||
|
lineNumber: getLineNumber(statements, index + 1),
|
||||||
|
startColumnNumber: getColNumber(statement, macroName),
|
||||||
|
endColumnNumber:
|
||||||
|
getColNumber(statement, macroName) + macroName.length,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (stack.length) {
|
||||||
|
diagnostics.push({
|
||||||
|
message: 'missing %mend statement for macro(s)',
|
||||||
|
lineNumber: statements.length + 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
export const hasMacroNameInMend: FileLintRule = {
|
||||||
|
type: LintRuleType.File,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
message,
|
||||||
|
test
|
||||||
|
}
|
||||||
@@ -40,6 +40,24 @@ 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 hasMacroNameInMend flag set', () => {
|
||||||
|
const config = new LintConfig({ hasMacroNameInMend: true })
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.lineLintRules.length).toEqual(0)
|
||||||
|
expect(config.fileLintRules.length).toEqual(1)
|
||||||
|
expect(config.fileLintRules[0].name).toEqual('hasMacroNameInMend')
|
||||||
|
expect(config.fileLintRules[0].type).toEqual(LintRuleType.File)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create an instance with the hasMacroNameInMend flag off', () => {
|
||||||
|
const config = new LintConfig({ hasMacroNameInMend: 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 })
|
||||||
|
|
||||||
@@ -58,18 +76,38 @@ describe('LintConfig', () => {
|
|||||||
const config = new LintConfig({
|
const config = new LintConfig({
|
||||||
noTrailingSpaces: true,
|
noTrailingSpaces: true,
|
||||||
noEncodedPasswords: true,
|
noEncodedPasswords: true,
|
||||||
hasDoxygenHeader: true
|
hasDoxygenHeader: true,
|
||||||
|
noSpacesInFileNames: true,
|
||||||
|
lowerCaseFileNames: true,
|
||||||
|
maxLineLength: 80,
|
||||||
|
noTabIndentation: true,
|
||||||
|
indentationMultiple: 2,
|
||||||
|
hasMacroNameInMend: true
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(config).toBeTruthy()
|
expect(config).toBeTruthy()
|
||||||
expect(config.lineLintRules.length).toEqual(2)
|
expect(config.lineLintRules.length).toEqual(5)
|
||||||
expect(config.lineLintRules[0].name).toEqual('noTrailingSpaces')
|
expect(config.lineLintRules[0].name).toEqual('noTrailingSpaces')
|
||||||
expect(config.lineLintRules[0].type).toEqual(LintRuleType.Line)
|
expect(config.lineLintRules[0].type).toEqual(LintRuleType.Line)
|
||||||
expect(config.lineLintRules[1].name).toEqual('noEncodedPasswords')
|
expect(config.lineLintRules[1].name).toEqual('noEncodedPasswords')
|
||||||
expect(config.lineLintRules[1].type).toEqual(LintRuleType.Line)
|
expect(config.lineLintRules[1].type).toEqual(LintRuleType.Line)
|
||||||
|
expect(config.lineLintRules[2].name).toEqual('noTabs')
|
||||||
|
expect(config.lineLintRules[2].type).toEqual(LintRuleType.Line)
|
||||||
|
expect(config.lineLintRules[3].name).toEqual('maxLineLength')
|
||||||
|
expect(config.lineLintRules[3].type).toEqual(LintRuleType.Line)
|
||||||
|
expect(config.lineLintRules[4].name).toEqual('indentationMultiple')
|
||||||
|
expect(config.lineLintRules[4].type).toEqual(LintRuleType.Line)
|
||||||
|
|
||||||
expect(config.fileLintRules.length).toEqual(1)
|
expect(config.fileLintRules.length).toEqual(2)
|
||||||
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].type).toEqual(LintRuleType.File)
|
||||||
|
|
||||||
|
expect(config.pathLintRules.length).toEqual(2)
|
||||||
|
expect(config.pathLintRules[0].name).toEqual('noSpacesInFileNames')
|
||||||
|
expect(config.pathLintRules[0].type).toEqual(LintRuleType.Path)
|
||||||
|
expect(config.pathLintRules[1].name).toEqual('lowerCaseFileNames')
|
||||||
|
expect(config.pathLintRules[1].type).toEqual(LintRuleType.Path)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { noEncodedPasswords } from '../rules/noEncodedPasswords'
|
|||||||
import { noSpacesInFileNames } from '../rules/noSpacesInFileNames'
|
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 { FileLintRule, LineLintRule, PathLintRule } from './LintRule'
|
import { FileLintRule, LineLintRule, PathLintRule } from './LintRule'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,5 +57,9 @@ export class LintConfig {
|
|||||||
if (json?.lowerCaseFileNames) {
|
if (json?.lowerCaseFileNames) {
|
||||||
this.pathLintRules.push(lowerCaseFileNames)
|
this.pathLintRules.push(lowerCaseFileNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (json?.hasMacroNameInMend) {
|
||||||
|
this.fileLintRules.push(hasMacroNameInMend)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export const DefaultLintConfiguration = {
|
|||||||
lowerCaseFileNames: true,
|
lowerCaseFileNames: true,
|
||||||
maxLineLength: 80,
|
maxLineLength: 80,
|
||||||
noTabIndentation: true,
|
noTabIndentation: true,
|
||||||
indentationMultiple: 2
|
indentationMultiple: 2,
|
||||||
|
hasMacroNameInMend: false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user