Skip to content

Commit

Permalink
Merge pull request #21 from sasjs/issue-12
Browse files Browse the repository at this point in the history
feat: new rule hasMacroNameInMend
  • Loading branch information
allanbowe authored Apr 6, 2021
2 parents 82bef9f + 443bdc0 commit 3970f05
Show file tree
Hide file tree
Showing 6 changed files with 422 additions and 5 deletions.
3 changes: 2 additions & 1 deletion .sasjslint
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
"maxLineLength": 80,
"lowerCaseFileNames": true,
"noTabIndentation": true,
"indentationMultiple": 2
"indentationMultiple": 2,
"hasMacroNameInMend": false
}
266 changes: 266 additions & 0 deletions src/rules/hasMacroNameInMend.spec.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 106 additions & 0 deletions src/rules/hasMacroNameInMend.ts
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 3970f05

Please sign in to comment.