-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #21 from sasjs/issue-12
feat: new rule hasMacroNameInMend
- Loading branch information
Showing
6 changed files
with
422 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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([]) | ||
}) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.