Skip to content

Commit

Permalink
Merge pull request #22 from sasjs/issue-16
Browse files Browse the repository at this point in the history
feat: new rules noNestedMacros & hasMacroParentheses
  • Loading branch information
allanbowe authored Apr 6, 2021
2 parents 39b8c4b + 524439f commit a9a3a67
Show file tree
Hide file tree
Showing 14 changed files with 438 additions and 40 deletions.
4 changes: 3 additions & 1 deletion .sasjslint
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@
"lowerCaseFileNames": true,
"noTabIndentation": true,
"indentationMultiple": 2,
"hasMacroNameInMend": false
"hasMacroNameInMend": false,
"noNestedMacros": true,
"hasMacroParentheses": true
}
4 changes: 2 additions & 2 deletions src/rules/hasMacroNameInMend.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('hasMacroNameInMend', () => {
message: 'mismatch macro name in %mend statement',
lineNumber: 4,
startColumnNumber: 9,
endColumnNumber: 25,
endColumnNumber: 24,
severity: Severity.Warning
}
])
Expand Down Expand Up @@ -219,7 +219,7 @@ describe('hasMacroNameInMend', () => {
message: 'mismatch macro name in %mend statement',
lineNumber: 6,
startColumnNumber: 14,
endColumnNumber: 30,
endColumnNumber: 29,
severity: Severity.Warning
}
])
Expand Down
39 changes: 6 additions & 33 deletions src/rules/hasMacroNameInMend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ 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 = 'hasMacroNameInMend'
const description = 'The %mend statement should contain the macro name'
Expand All @@ -22,8 +25,8 @@ const test = (value: string) => {

if (trimmedStatement.startsWith('%macro ')) {
const macroName = trimmedStatement
.split(' ')
.filter((s: string) => !!s)[1]
.slice(7, trimmedStatement.length)
.trim()
.split('(')[0]
stack.push(macroName)
} else if (trimmedStatement.startsWith('%mend')) {
Expand All @@ -46,7 +49,7 @@ const test = (value: string) => {
lineNumber: getLineNumber(statements, index + 1),
startColumnNumber: getColNumber(statement, macroName),
endColumnNumber:
getColNumber(statement, macroName) + macroName.length,
getColNumber(statement, macroName) + macroName.length - 1,
severity: Severity.Warning
})
}
Expand All @@ -64,36 +67,6 @@ const test = (value: string) => {
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.
*/
Expand Down
128 changes: 128 additions & 0 deletions src/rules/hasMacroParentheses.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Severity } from '../types/Severity'
import { hasMacroParentheses } from './hasMacroParentheses'

describe('hasMacroParentheses', () => {
it('should return an empty array when macro defined correctly', () => {
const content = `
%macro somemacro();
%put &sysmacroname;
%mend somemacro;`

expect(hasMacroParentheses.test(content)).toEqual([])
})

it('should return an array with a single diagnostics when macro defined without parentheses', () => {
const content = `
%macro somemacro;
%put &sysmacroname;
%mend somemacro;`

expect(hasMacroParentheses.test(content)).toEqual([
{
message: 'Macro definition missing parentheses',
lineNumber: 2,
startColumnNumber: 10,
endColumnNumber: 18,
severity: Severity.Warning
}
])
})

it('should return an array with a single diagnostics when macro defined without name', () => {
const content = `
%macro ();
%put &sysmacroname;
%mend;`

expect(hasMacroParentheses.test(content)).toEqual([
{
message: 'Macro definition missing name',
lineNumber: 2,
startColumnNumber: 3,
endColumnNumber: 12,
severity: Severity.Warning
}
])
})

it('should return an array with a single diagnostics when macro defined without name and parentheses', () => {
const content = `
%macro ;
%put &sysmacroname;
%mend;`

expect(hasMacroParentheses.test(content)).toEqual([
{
message: 'Macro definition missing name',
lineNumber: 2,
startColumnNumber: 3,
endColumnNumber: 10,
severity: Severity.Warning
}
])
})

it('should return an empty array when the file is undefined', () => {
const content = undefined

expect(hasMacroParentheses.test((content as unknown) as string)).toEqual([])
})

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(hasMacroParentheses.test(content)).toEqual([])
})

it('should return an array with a single diagnostic when macro defined without parentheses 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(hasMacroParentheses.test(content)).toEqual([
{
message: 'Macro definition missing parentheses',
lineNumber: 19,
startColumnNumber: 12,
endColumnNumber: 23,
severity: Severity.Warning
}
])
})
})
})
77 changes: 77 additions & 0 deletions src/rules/hasMacroParentheses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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 (!macroName)
diagnostics.push({
message: 'Macro definition missing name',
lineNumber: getLineNumber(statements, index + 1),
startColumnNumber: getColNumber(statement, '%macro'),
endColumnNumber: statement.length,
severity: Severity.Warning
})
else 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
}
78 changes: 78 additions & 0 deletions src/rules/noNestedMacros.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Severity } from '../types/Severity'
import { noNestedMacros } from './noNestedMacros'

describe('noNestedMacros', () => {
it('should return an empty array when no nested macro', () => {
const content = `
%macro somemacro();
%put &sysmacroname;
%mend somemacro;`

expect(noNestedMacros.test(content)).toEqual([])
})

it('should return an array with a single diagnostics when nested macro defined', () => {
const content = `
%macro outer();
/* any amount of arbitrary code */
%macro inner();
%put inner;
%mend;
%inner()
%put outer;
%mend;
%outer()`

expect(noNestedMacros.test(content)).toEqual([
{
message: "Macro definition present inside another macro 'outer'",
lineNumber: 4,
startColumnNumber: 7,
endColumnNumber: 20,
severity: Severity.Warning
}
])
})

it('should return an array with a single diagnostics when nested macro defined 2 levels', () => {
const content = `
%macro outer();
/* any amount of arbitrary code */
%macro inner();
%put inner;
%macro inner2();
%put inner2;
%mend;
%mend;
%inner()
%put outer;
%mend;
%outer()`

expect(noNestedMacros.test(content)).toEqual([
{
message: "Macro definition present inside another macro 'outer'",
lineNumber: 4,
startColumnNumber: 7,
endColumnNumber: 20,
severity: Severity.Warning
},
{
message: "Macro definition present inside another macro 'inner'",
lineNumber: 7,
startColumnNumber: 17,
endColumnNumber: 31,
severity: Severity.Warning
}
])
})

it('should return an empty array when the file is undefined', () => {
const content = undefined

expect(noNestedMacros.test((content as unknown) as string)).toEqual([])
})
})
Loading

0 comments on commit a9a3a67

Please sign in to comment.