From d7721f8e5e54fe83955cfa3e56aaf0020e641522 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Thu, 20 May 2021 22:41:16 +0500 Subject: [PATCH 01/11] fix: comments within/outside the statement --- src/utils/trimComments.spec.ts | 9 +++++++++ src/utils/trimComments.ts | 12 ++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/utils/trimComments.spec.ts b/src/utils/trimComments.spec.ts index 4ede9c3..712b770 100644 --- a/src/utils/trimComments.spec.ts +++ b/src/utils/trimComments.spec.ts @@ -7,6 +7,15 @@ describe('trimComments', () => { /* some comment */ some code; `) ).toEqual({ statement: 'some code;', commentStarted: false }) + + expect( + trimComments(`/* some comment */ + /* some comment */ CODE_Keyword1 /* some comment */ CODE_Keyword2/* some comment */;/* some comment */ + /* some comment */`) + ).toEqual({ + statement: 'CODE_Keyword1 CODE_Keyword2;', + commentStarted: false + }) }) it('should return statment, having multi-line comment', () => { diff --git a/src/utils/trimComments.ts b/src/utils/trimComments.ts index 670de7a..a6b5d2a 100644 --- a/src/utils/trimComments.ts +++ b/src/utils/trimComments.ts @@ -17,6 +17,18 @@ export const trimComments = ( } else { return { statement: '', commentStarted: true } } + } else if (trimmed.includes('/*')) { + const statementBeforeCommentStarts = trimmed.slice(0, trimmed.indexOf('/*')) + const remainingStatement = trimmed.slice( + trimmed.indexOf('/*'), + trimmed.length + ) + + const result = trimComments(remainingStatement, false) + return { + statement: statementBeforeCommentStarts + result.statement, + commentStarted: result.commentStarted + } } return { statement: trimmed, commentStarted: false } } From 8bfb54742769ac683f98a9350af522009529a9ef Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Thu, 20 May 2021 23:00:59 +0500 Subject: [PATCH 02/11] chore: removed redundant property from parsedMacros --- src/utils/parseMacros.spec.ts | 8 -------- src/utils/parseMacros.ts | 3 --- 2 files changed, 11 deletions(-) diff --git a/src/utils/parseMacros.spec.ts b/src/utils/parseMacros.spec.ts index ce33f37..a238ff0 100644 --- a/src/utils/parseMacros.spec.ts +++ b/src/utils/parseMacros.spec.ts @@ -20,7 +20,6 @@ describe('parseMacros', () => { endLineNumber: 3, parentMacro: '', hasMacroNameInMend: false, - hasParentheses: false, mismatchedMendMacroName: '' }) }) @@ -43,7 +42,6 @@ describe('parseMacros', () => { endLineNumber: 3, parentMacro: '', hasMacroNameInMend: false, - hasParentheses: false, mismatchedMendMacroName: '' }) }) @@ -66,7 +64,6 @@ describe('parseMacros', () => { endLineNumber: 3, parentMacro: '', hasMacroNameInMend: false, - hasParentheses: false, mismatchedMendMacroName: '' }) }) @@ -90,7 +87,6 @@ describe('parseMacros', () => { endLineNumber: 4, parentMacro: '', hasMacroNameInMend: false, - hasParentheses: false, mismatchedMendMacroName: '' }) }) @@ -116,7 +112,6 @@ describe('parseMacros', () => { endLineNumber: 3, parentMacro: '', hasMacroNameInMend: false, - hasParentheses: false, mismatchedMendMacroName: '' }) expect(macros).toContainEqual({ @@ -129,7 +124,6 @@ describe('parseMacros', () => { endLineNumber: 6, parentMacro: '', hasMacroNameInMend: true, - hasParentheses: true, mismatchedMendMacroName: '' }) }) @@ -155,7 +149,6 @@ describe('parseMacros', () => { endLineNumber: 6, parentMacro: '', hasMacroNameInMend: true, - hasParentheses: true, mismatchedMendMacroName: '' }) expect(macros).toContainEqual({ @@ -168,7 +161,6 @@ describe('parseMacros', () => { endLineNumber: 5, parentMacro: 'test', hasMacroNameInMend: false, - hasParentheses: false, mismatchedMendMacroName: '' }) }) diff --git a/src/utils/parseMacros.ts b/src/utils/parseMacros.ts index 656dc41..89728ba 100644 --- a/src/utils/parseMacros.ts +++ b/src/utils/parseMacros.ts @@ -12,7 +12,6 @@ interface Macro { termination: string parentMacro: string hasMacroNameInMend: boolean - hasParentheses: boolean mismatchedMendMacroName: string } @@ -52,7 +51,6 @@ export const parseMacros = (text: string, config?: LintConfig): Macro[] => { parentMacro: macroStack.length ? macroStack[macroStack.length - 1].name : '', - hasParentheses: trimmedStatement.endsWith('()'), hasMacroNameInMend: false, mismatchedMendMacroName: '', declarationLine: line, @@ -79,7 +77,6 @@ export const parseMacros = (text: string, config?: LintConfig): Macro[] => { startLineNumber: null, endLineNumber: index + 1, parentMacro: '', - hasParentheses: false, hasMacroNameInMend: false, mismatchedMendMacroName: '', declarationLine: '', From af2d2c12c19bae9f18cbd477889f7b7aa7f2e37c Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Fri, 21 May 2021 05:10:34 +0500 Subject: [PATCH 03/11] feat: mult-line macro declarations --- src/format/formatText.spec.ts | 8 +- src/rules/file/hasMacroNameInMend.spec.ts | 2 +- src/rules/file/hasMacroNameInMend.ts | 16 +- src/rules/file/hasMacroParentheses.spec.ts | 26 ++-- src/rules/file/hasMacroParentheses.ts | 50 +++--- src/rules/file/noNestedMacros.ts | 8 +- src/rules/line/strictMacroDefinition.spec.ts | 29 +++- src/rules/line/strictMacroDefinition.ts | 26 +++- src/utils/parseMacros.spec.ts | 154 ++++++++++++++++--- src/utils/parseMacros.ts | 78 ++++++++-- src/utils/trimComments.spec.ts | 2 +- src/utils/trimComments.ts | 14 +- 12 files changed, 326 insertions(+), 87 deletions(-) diff --git a/src/format/formatText.spec.ts b/src/format/formatText.spec.ts index 35f2a53..2cccf0d 100644 --- a/src/format/formatText.spec.ts +++ b/src/format/formatText.spec.ts @@ -12,14 +12,14 @@ describe('formatText', () => { new LintConfig(getLintConfigModule.DefaultLintConfiguration) ) ) - const text = `%macro test + const text = `%macro test; %put 'hello';\r\n%mend; ` const expectedOutput = `/** @file @brief

SAS Macros

-**/\n%macro test +**/\n%macro test; %put 'hello';\n%mend test;` const output = await formatText(text) @@ -38,9 +38,9 @@ describe('formatText', () => { }) ) ) - const text = `%macro test\n %put 'hello';\r\n%mend; ` + const text = `%macro test;\n %put 'hello';\r\n%mend; ` - const expectedOutput = `/**\r\n @file\r\n @brief \r\n

SAS Macros

\r\n**/\r\n%macro test\r\n %put 'hello';\r\n%mend test;` + const expectedOutput = `/**\r\n @file\r\n @brief \r\n

SAS Macros

\r\n**/\r\n%macro test;\r\n %put 'hello';\r\n%mend test;` const output = await formatText(text) diff --git a/src/rules/file/hasMacroNameInMend.spec.ts b/src/rules/file/hasMacroNameInMend.spec.ts index 5693a2e..8acf000 100644 --- a/src/rules/file/hasMacroNameInMend.spec.ts +++ b/src/rules/file/hasMacroNameInMend.spec.ts @@ -56,7 +56,7 @@ describe('hasMacroNameInMend - test', () => { it('should return an array with a diagnostic for each macro missing an %mend statement', () => { const content = `%macro somemacro; %put &sysmacroname; - %macro othermacro` + %macro othermacro;` expect(hasMacroNameInMend.test(content)).toEqual([ { diff --git a/src/rules/file/hasMacroNameInMend.ts b/src/rules/file/hasMacroNameInMend.ts index cab7ac1..0a37100 100644 --- a/src/rules/file/hasMacroNameInMend.ts +++ b/src/rules/file/hasMacroNameInMend.ts @@ -17,7 +17,7 @@ const test = (value: string, config?: LintConfig) => { const macros = parseMacros(value, config) const diagnostics: Diagnostic[] = [] macros.forEach((macro) => { - if (macro.startLineNumber === null && macro.endLineNumber !== null) { + if (macro.startLineNumbers.length === 0 && macro.endLineNumber !== null) { const endLine = lines[macro.endLineNumber - 1] diagnostics.push({ message: `%mend statement is redundant`, @@ -27,10 +27,13 @@ const test = (value: string, config?: LintConfig) => { getColumnNumber(endLine, '%mend') + macro.termination.length, severity: Severity.Warning }) - } else if (macro.endLineNumber === null && macro.startLineNumber !== null) { + } else if ( + macro.endLineNumber === null && + macro.startLineNumbers.length !== 0 + ) { diagnostics.push({ message: `Missing %mend statement for macro - ${macro.name}`, - lineNumber: macro.startLineNumber, + lineNumber: macro.startLineNumbers![0], startColumnNumber: 1, endColumnNumber: 1, severity: Severity.Warning @@ -73,7 +76,7 @@ const fix = (value: string, config?: LintConfig): string => { const macros = parseMacros(value, config) macros.forEach((macro) => { - if (macro.startLineNumber === null && macro.endLineNumber !== null) { + if (macro.startLineNumbers.length === 0 && macro.endLineNumber !== null) { // %mend statement is redundant const endLine = lines[macro.endLineNumber - 1] const startColumnNumber = getColumnNumber(endLine, '%mend') @@ -83,7 +86,10 @@ const fix = (value: string, config?: LintConfig): string => { const beforeStatement = endLine.slice(0, startColumnNumber - 1) const afterStatement = endLine.slice(endColumnNumber) lines[macro.endLineNumber - 1] = beforeStatement + afterStatement - } else if (macro.endLineNumber === null && macro.startLineNumber !== null) { + } else if ( + macro.endLineNumber === null && + macro.startLineNumbers.length !== 0 + ) { // missing %mend statement } else if (macro.mismatchedMendMacroName) { // mismatched macro name diff --git a/src/rules/file/hasMacroParentheses.spec.ts b/src/rules/file/hasMacroParentheses.spec.ts index 1fa58bc..3ace5ce 100644 --- a/src/rules/file/hasMacroParentheses.spec.ts +++ b/src/rules/file/hasMacroParentheses.spec.ts @@ -140,17 +140,17 @@ describe('hasMacroParentheses', () => { }) }) - it('should return an array with a single diagnostic when a macro definition contains a space', () => { - const content = `%macro test ()` - - expect(hasMacroParentheses.test(content)).toEqual([ - { - message: 'Macro definition contains space(s)', - lineNumber: 1, - startColumnNumber: 8, - endColumnNumber: 14, - severity: Severity.Warning - } - ]) - }) + // it('should return an array with a single diagnostic when a macro definition contains a space', () => { + // const content = `%macro test ()` + + // expect(hasMacroParentheses.test(content)).toEqual([ + // { + // message: 'Macro definition contains space(s)', + // lineNumber: 1, + // startColumnNumber: 8, + // endColumnNumber: 14, + // severity: Severity.Warning + // } + // ]) + // }) }) diff --git a/src/rules/file/hasMacroParentheses.ts b/src/rules/file/hasMacroParentheses.ts index 7975cba..13d15e5 100644 --- a/src/rules/file/hasMacroParentheses.ts +++ b/src/rules/file/hasMacroParentheses.ts @@ -16,36 +16,48 @@ const test = (value: string, config?: LintConfig) => { if (!macro.name) { diagnostics.push({ message: 'Macro definition missing name', - lineNumber: macro.startLineNumber!, - startColumnNumber: getColumnNumber(macro.declarationLine, '%macro'), + lineNumber: macro.startLineNumbers![0], + startColumnNumber: getColumnNumber( + macro.declarationLines![0], + '%macro' + ), endColumnNumber: - getColumnNumber(macro.declarationLine, '%macro') + + getColumnNumber(macro.declarationLines![0], '%macro') + macro.declaration.length, severity: Severity.Warning }) - } else if (!macro.declarationLine.includes('(')) { + } else if (!macro.declarationLines.find((dl) => dl.includes('('))) { + const macroNameLineIndex = macro.declarationLines.findIndex((dl) => + dl.includes(macro.name) + ) diagnostics.push({ message, - lineNumber: macro.startLineNumber!, - startColumnNumber: getColumnNumber(macro.declarationLine, macro.name), + lineNumber: macro.startLineNumbers![macroNameLineIndex], + startColumnNumber: getColumnNumber( + macro.declarationLines[macroNameLineIndex], + macro.name + ), endColumnNumber: - getColumnNumber(macro.declarationLine, macro.name) + + getColumnNumber( + macro.declarationLines[macroNameLineIndex], + macro.name + ) + macro.name.length - 1, severity: Severity.Warning }) - } else if (macro.name !== macro.name.trim()) { - diagnostics.push({ - message: 'Macro definition contains space(s)', - lineNumber: macro.startLineNumber!, - startColumnNumber: getColumnNumber(macro.declarationLine, macro.name), - endColumnNumber: - getColumnNumber(macro.declarationLine, macro.name) + - macro.name.length - - 1 + - `()`.length, - severity: Severity.Warning - }) + // } else if (macro.name !== macro.name.trim()) { + // diagnostics.push({ + // message: 'Macro definition contains space(s)', + // lineNumber: macro.startLineNumber!, + // startColumnNumber: getColumnNumber(macro.declarationLine, macro.name), + // endColumnNumber: + // getColumnNumber(macro.declarationLine, macro.name) + + // macro.name.length - + // 1 + + // `()`.length, + // severity: Severity.Warning + // }) } }) diff --git a/src/rules/file/noNestedMacros.ts b/src/rules/file/noNestedMacros.ts index 338ae15..b255545 100644 --- a/src/rules/file/noNestedMacros.ts +++ b/src/rules/file/noNestedMacros.ts @@ -22,17 +22,17 @@ const test = (value: string, config?: LintConfig) => { message: message .replace('{macro}', macro.name) .replace('{parent}', macro.parentMacro), - lineNumber: macro.startLineNumber as number, + lineNumber: macro.startLineNumbers![0] as number, startColumnNumber: getColumnNumber( - lines[(macro.startLineNumber as number) - 1], + lines[(macro.startLineNumbers![0] as number) - 1], '%macro' ), endColumnNumber: getColumnNumber( - lines[(macro.startLineNumber as number) - 1], + lines[(macro.startLineNumbers![0] as number) - 1], '%macro' ) + - lines[(macro.startLineNumber as number) - 1].trim().length - + lines[(macro.startLineNumbers![0] as number) - 1].trim().length - 1, severity: Severity.Warning }) diff --git a/src/rules/line/strictMacroDefinition.spec.ts b/src/rules/line/strictMacroDefinition.spec.ts index 63b0a4e..750084d 100644 --- a/src/rules/line/strictMacroDefinition.spec.ts +++ b/src/rules/line/strictMacroDefinition.spec.ts @@ -24,6 +24,19 @@ describe('strictMacroDefinition', () => { const line7 = ' /* Some Comment */ %macro somemacro(var1, var2) /minoperator ; /* Some Comment */' expect(strictMacroDefinition.test(line7, 1)).toEqual([]) + + const line8 = + '%macro macroName( arr, arr/* / store source */3 ) /* / store source */;/* / store source */' + expect(strictMacroDefinition.test(line8, 1)).toEqual([]) + + const line9 = '%macro macroName(var1, var2=with space, var3=);' + expect(strictMacroDefinition.test(line9, 1)).toEqual([]) + + const line10 = '%macro macroName()/ /* some comment */ store source;' + expect(strictMacroDefinition.test(line10, 1)).toEqual([]) + + const line11 = '`%macro macroName() /* / store source */;' + expect(strictMacroDefinition.test(line11, 1)).toEqual([]) }) it('should return an array with a single diagnostic when Macro definition has space in param', () => { @@ -39,7 +52,7 @@ describe('strictMacroDefinition', () => { ]) }) - it('should return an array with a two diagnostics when Macro definition has space in param', () => { + it('should return an array with a two diagnostics when Macro definition has space in params', () => { const line = '%macro somemacro(var1, var 2, v ar3, var4);' expect(strictMacroDefinition.test(line, 1)).toEqual([ { @@ -59,6 +72,20 @@ describe('strictMacroDefinition', () => { ]) }) + it('should return an array with a two diagnostics when Macro definition has space in params - special case', () => { + const line = + '%macro macroName( arr, ar r/* / store source */ 3 ) /* / store source */;/* / store source */' + expect(strictMacroDefinition.test(line, 1)).toEqual([ + { + message: `Param 'ar r 3' cannot have space`, + lineNumber: 1, + startColumnNumber: 24, + endColumnNumber: 49, + severity: Severity.Warning + } + ]) + }) + it('should return an array with a single diagnostic when Macro definition has invalid option', () => { const line = '%macro somemacro(var1, var2)/minXoperator;' expect(strictMacroDefinition.test(line, 1)).toEqual([ diff --git a/src/rules/line/strictMacroDefinition.ts b/src/rules/line/strictMacroDefinition.ts index 9adc3cf..246ba7f 100644 --- a/src/rules/line/strictMacroDefinition.ts +++ b/src/rules/line/strictMacroDefinition.ts @@ -42,12 +42,34 @@ const test = (value: string, lineNumber: number) => { const params = paramsTrimmed.split(',') params.forEach((param) => { const trimedParam = param.split('=')[0].trim() + + let paramStartIndex: number = 1, + paramEndIndex: number = value.length + + if (value.indexOf(trimedParam) === -1) { + const comment = '/\\*(.*?)\\*/' + for (let i = 1; i < trimedParam.length; i++) { + const paramWithComment = + trimedParam.slice(0, i) + comment + trimedParam.slice(i) + const regEx = new RegExp(paramWithComment) + const result = regEx.exec(value) + if (result) { + paramStartIndex = value.indexOf(result[0]) + paramEndIndex = value.indexOf(result[0]) + result[0].length + break + } + } + } else { + paramStartIndex = value.indexOf(trimedParam) + paramEndIndex = value.indexOf(trimedParam) + trimedParam.length + } + if (trimedParam.includes(' ')) { diagnostics.push({ message: `Param '${trimedParam}' cannot have space`, lineNumber, - startColumnNumber: value.indexOf(trimedParam) + 1, - endColumnNumber: value.indexOf(trimedParam) + trimedParam.length, + startColumnNumber: paramStartIndex + 1, + endColumnNumber: paramEndIndex, severity: Severity.Warning }) } diff --git a/src/utils/parseMacros.spec.ts b/src/utils/parseMacros.spec.ts index a238ff0..e8f25ce 100644 --- a/src/utils/parseMacros.spec.ts +++ b/src/utils/parseMacros.spec.ts @@ -3,7 +3,7 @@ import { parseMacros } from './parseMacros' describe('parseMacros', () => { it('should return an array with a single macro', () => { - const text = `%macro test; + const text = ` %macro test; %put 'hello'; %mend` @@ -12,11 +12,11 @@ describe('parseMacros', () => { expect(macros.length).toEqual(1) expect(macros).toContainEqual({ name: 'test', - declarationLine: '%macro test;', + declarationLines: [' %macro test;'], terminationLine: '%mend', declaration: '%macro test', termination: '%mend', - startLineNumber: 1, + startLineNumbers: [1], endLineNumber: 3, parentMacro: '', hasMacroNameInMend: false, @@ -34,11 +34,11 @@ describe('parseMacros', () => { expect(macros.length).toEqual(1) expect(macros).toContainEqual({ name: 'test', - declarationLine: '%macro test(var,sum);', + declarationLines: ['%macro test(var,sum);'], terminationLine: '%mend', declaration: '%macro test(var,sum)', termination: '%mend', - startLineNumber: 1, + startLineNumbers: [1], endLineNumber: 3, parentMacro: '', hasMacroNameInMend: false, @@ -56,11 +56,11 @@ describe('parseMacros', () => { expect(macros.length).toEqual(1) expect(macros).toContainEqual({ name: 'test', - declarationLine: '%macro test/parmbuff;', + declarationLines: ['%macro test/parmbuff;'], terminationLine: '%mend', declaration: '%macro test/parmbuff', termination: '%mend', - startLineNumber: 1, + startLineNumbers: [1], endLineNumber: 3, parentMacro: '', hasMacroNameInMend: false, @@ -79,11 +79,15 @@ describe('parseMacros', () => { expect(macros.length).toEqual(1) expect(macros).toContainEqual({ name: 'foobar', - declarationLine: '/* commentary */ %macro foobar(arg) /store source', + declarationLines: [ + '/* commentary */ %macro foobar(arg) /store source', + ' des="This macro does not do much";' + ], terminationLine: '%mend', - declaration: '%macro foobar(arg) /store source', + declaration: + '%macro foobar(arg) /store source des="This macro does not do much"', termination: '%mend', - startLineNumber: 1, + startLineNumbers: [1, 2], endLineNumber: 4, parentMacro: '', hasMacroNameInMend: false, @@ -104,11 +108,11 @@ describe('parseMacros', () => { expect(macros.length).toEqual(2) expect(macros).toContainEqual({ name: 'foo', - declarationLine: '%macro foo;', + declarationLines: ['%macro foo;'], terminationLine: '%mend;', declaration: '%macro foo', termination: '%mend', - startLineNumber: 1, + startLineNumbers: [1], endLineNumber: 3, parentMacro: '', hasMacroNameInMend: false, @@ -116,11 +120,11 @@ describe('parseMacros', () => { }) expect(macros).toContainEqual({ name: 'bar', - declarationLine: '%macro bar();', + declarationLines: ['%macro bar();'], terminationLine: '%mend bar;', declaration: '%macro bar()', termination: '%mend bar', - startLineNumber: 4, + startLineNumbers: [4], endLineNumber: 6, parentMacro: '', hasMacroNameInMend: true, @@ -129,9 +133,9 @@ describe('parseMacros', () => { }) it('should detect nested macro definitions', () => { - const text = `%macro test() + const text = `%macro test(); %put 'hello'; - %macro test2 + %macro test2; %put 'world; %mend %mend test` @@ -141,11 +145,11 @@ describe('parseMacros', () => { expect(macros.length).toEqual(2) expect(macros).toContainEqual({ name: 'test', - declarationLine: '%macro test()', + declarationLines: ['%macro test();'], terminationLine: '%mend test', declaration: '%macro test()', termination: '%mend test', - startLineNumber: 1, + startLineNumbers: [1], endLineNumber: 6, parentMacro: '', hasMacroNameInMend: true, @@ -153,15 +157,125 @@ describe('parseMacros', () => { }) expect(macros).toContainEqual({ name: 'test2', - declarationLine: ' %macro test2', + declarationLines: [' %macro test2;'], terminationLine: ' %mend', declaration: '%macro test2', termination: '%mend', - startLineNumber: 3, + startLineNumbers: [3], endLineNumber: 5, parentMacro: 'test', hasMacroNameInMend: false, mismatchedMendMacroName: '' }) }) + + describe(`multi-line macro declarations`, () => { + it('should return an array with a single macro', () => { + const text = `%macro + test; + %put 'hello'; +%mend` + + const macros = parseMacros(text, new LintConfig()) + + expect(macros.length).toEqual(1) + expect(macros).toContainEqual({ + name: 'test', + declarationLines: ['%macro ', ' test;'], + terminationLine: '%mend', + declaration: '%macro test', + termination: '%mend', + startLineNumbers: [1, 2], + endLineNumber: 4, + parentMacro: '', + hasMacroNameInMend: false, + mismatchedMendMacroName: '' + }) + }) + + it('should return an array with a single macro having parameters', () => { + const text = `%macro + test( + var, + sum);%put 'hello'; +%mend` + + const macros = parseMacros(text, new LintConfig()) + + expect(macros.length).toEqual(1) + expect(macros).toContainEqual({ + name: 'test', + declarationLines: [ + '%macro ', + ` test(`, + ` var,`, + ` sum);%put 'hello';` + ], + terminationLine: '%mend', + declaration: '%macro test( var, sum)', + termination: '%mend', + startLineNumbers: [1, 2, 3, 4], + endLineNumber: 5, + parentMacro: '', + hasMacroNameInMend: false, + mismatchedMendMacroName: '' + }) + }) + + it('should return an array with a single macro having PARMBUFF option', () => { + const text = `%macro test + /parmbuff; + %put 'hello'; +%mend` + + const macros = parseMacros(text, new LintConfig()) + + expect(macros.length).toEqual(1) + expect(macros).toContainEqual({ + name: 'test', + declarationLines: ['%macro test', ' /parmbuff;'], + terminationLine: '%mend', + declaration: '%macro test /parmbuff', + termination: '%mend', + startLineNumbers: [1, 2], + endLineNumber: 4, + parentMacro: '', + hasMacroNameInMend: false, + mismatchedMendMacroName: '' + }) + }) + + it('should return an array with a single macro having paramerter & SOURCE option', () => { + const text = `/* commentary */ %macro foobar/* commentary */(arg) + /* commentary */ + /store + /* commentary */source + des="This macro does not do much"; + %put 'hello'; +%mend` + + const macros = parseMacros(text, new LintConfig()) + + expect(macros.length).toEqual(1) + expect(macros).toContainEqual({ + name: 'foobar', + declarationLines: [ + '/* commentary */ %macro foobar/* commentary */(arg) ', + ' /* commentary */', + ' /store', + ' /* commentary */source', + ' des="This macro does not do much";' + ], + terminationLine: '%mend', + declaration: + '%macro foobar(arg) /store source des="This macro does not do much"', + termination: '%mend', + startLineNumbers: [1, 2, 3, 4, 5], + endLineNumber: 7, + parentMacro: '', + hasMacroNameInMend: false, + mismatchedMendMacroName: '' + }) + }) + }) }) diff --git a/src/utils/parseMacros.ts b/src/utils/parseMacros.ts index 89728ba..591dc50 100644 --- a/src/utils/parseMacros.ts +++ b/src/utils/parseMacros.ts @@ -4,9 +4,9 @@ import { trimComments } from './trimComments' interface Macro { name: string - startLineNumber: number | null + startLineNumbers: number[] endLineNumber: number | null - declarationLine: string + declarationLines: string[] terminationLine: string declaration: string termination: string @@ -22,38 +22,92 @@ export const parseMacros = (text: string, config?: LintConfig): Macro[] => { let isCommentStarted = false let macroStack: Macro[] = [] - lines.forEach((line, index) => { + let isReadingMacroDefinition = false + let isStatementContinues = true + let tempMacroDeclaration = '' + let tempMacroDeclarationLines: string[] = [] + let tempStartLineNumbers: number[] = [] + lines.forEach((line, lineIndex) => { const { statement: trimmedLine, commentStarted } = trimComments( line, isCommentStarted ) isCommentStarted = commentStarted - const statements: string[] = trimmedLine ? trimmedLine.split(';') : [] - statements.forEach((statement) => { + isStatementContinues = !trimmedLine.endsWith(';') + + const statements: string[] = trimmedLine.split(';') + + statements.forEach((statement, statementIndex) => { const { statement: trimmedStatement, commentStarted } = trimComments( statement, isCommentStarted ) isCommentStarted = commentStarted + if (isReadingMacroDefinition) { + tempMacroDeclaration = + tempMacroDeclaration + + (trimmedStatement ? ' ' + trimmedStatement : '') + tempMacroDeclarationLines.push(line) + tempStartLineNumbers.push(lineIndex + 1) + + if (!Object.is(statements.length - 1, statementIndex)) { + isReadingMacroDefinition = false + + const name = tempMacroDeclaration + .slice(7, tempMacroDeclaration.length) + .trim() + .split('/')[0] + .split('(')[0] + .trim() + macroStack.push({ + name, + startLineNumbers: tempStartLineNumbers, + endLineNumber: null, + parentMacro: macroStack.length + ? macroStack[macroStack.length - 1].name + : '', + hasMacroNameInMend: false, + mismatchedMendMacroName: '', + declarationLines: tempMacroDeclarationLines, + terminationLine: '', + declaration: tempMacroDeclaration, + termination: '' + }) + } + } + if (trimmedStatement.startsWith('%macro')) { - const startLineNumber = index + 1 + const startLineNumber = lineIndex + 1 + + if ( + isStatementContinues && + Object.is(statements.length - 1, statementIndex) + ) { + tempMacroDeclaration = trimmedStatement + tempMacroDeclarationLines = [line] + tempStartLineNumbers = [startLineNumber] + isReadingMacroDefinition = true + return + } + const name = trimmedStatement .slice(7, trimmedStatement.length) .trim() .split('/')[0] .split('(')[0] + .trim() macroStack.push({ name, - startLineNumber, + startLineNumbers: [startLineNumber], endLineNumber: null, parentMacro: macroStack.length ? macroStack[macroStack.length - 1].name : '', hasMacroNameInMend: false, mismatchedMendMacroName: '', - declarationLine: line, + declarationLines: [line], terminationLine: '', declaration: trimmedStatement, termination: '' @@ -63,7 +117,7 @@ export const parseMacros = (text: string, config?: LintConfig): Macro[] => { const macro = macroStack.pop() as Macro const mendMacroName = trimmedStatement.split(' ').filter((s: string) => !!s)[1] || '' - macro.endLineNumber = index + 1 + macro.endLineNumber = lineIndex + 1 macro.hasMacroNameInMend = mendMacroName === macro.name macro.mismatchedMendMacroName = macro.hasMacroNameInMend ? '' @@ -74,12 +128,12 @@ export const parseMacros = (text: string, config?: LintConfig): Macro[] => { } else { macros.push({ name: '', - startLineNumber: null, - endLineNumber: index + 1, + startLineNumbers: [], + endLineNumber: lineIndex + 1, parentMacro: '', hasMacroNameInMend: false, mismatchedMendMacroName: '', - declarationLine: '', + declarationLines: [], terminationLine: line, declaration: '', termination: trimmedStatement diff --git a/src/utils/trimComments.spec.ts b/src/utils/trimComments.spec.ts index 712b770..1a1b169 100644 --- a/src/utils/trimComments.spec.ts +++ b/src/utils/trimComments.spec.ts @@ -13,7 +13,7 @@ describe('trimComments', () => { /* some comment */ CODE_Keyword1 /* some comment */ CODE_Keyword2/* some comment */;/* some comment */ /* some comment */`) ).toEqual({ - statement: 'CODE_Keyword1 CODE_Keyword2;', + statement: 'CODE_Keyword1 CODE_Keyword2;', commentStarted: false }) }) diff --git a/src/utils/trimComments.ts b/src/utils/trimComments.ts index a6b5d2a..6660435 100644 --- a/src/utils/trimComments.ts +++ b/src/utils/trimComments.ts @@ -1,8 +1,9 @@ export const trimComments = ( statement: string, - commentStarted: boolean = false + commentStarted: boolean = false, + trimEnd: boolean = false ): { statement: string; commentStarted: boolean } => { - let trimmed = (statement || '').trim() + let trimmed = trimEnd ? (statement || '').trimEnd() : (statement || '').trim() if (commentStarted || trimmed.startsWith('/*')) { const parts = trimmed.split('*/') @@ -20,13 +21,16 @@ export const trimComments = ( } else if (trimmed.includes('/*')) { const statementBeforeCommentStarts = trimmed.slice(0, trimmed.indexOf('/*')) const remainingStatement = trimmed.slice( - trimmed.indexOf('/*'), + trimmed.indexOf('*/') + 2, trimmed.length ) - const result = trimComments(remainingStatement, false) + const result = trimComments(remainingStatement, false, true) + const completeStatement = statementBeforeCommentStarts + result.statement return { - statement: statementBeforeCommentStarts + result.statement, + statement: trimEnd + ? completeStatement.trimEnd() + : completeStatement.trim(), commentStarted: result.commentStarted } } From f793eb3a76e5284e8231747a0849f303f56ba449 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Fri, 21 May 2021 17:29:23 +0500 Subject: [PATCH 04/11] fix(strictMacroDefinition): moved from lineRules to fileRules --- src/rules/file/index.ts | 1 + src/rules/file/strictMacroDefinition.spec.ts | 204 +++++++++++++++++++ src/rules/file/strictMacroDefinition.ts | 140 +++++++++++++ src/rules/line/index.ts | 1 - src/rules/line/strictMacroDefinition.spec.ts | 115 ----------- src/rules/line/strictMacroDefinition.ts | 110 ---------- src/types/LintConfig.ts | 8 +- src/utils/getLintConfig.spec.ts | 4 +- 8 files changed, 351 insertions(+), 232 deletions(-) create mode 100644 src/rules/file/strictMacroDefinition.spec.ts create mode 100644 src/rules/file/strictMacroDefinition.ts delete mode 100644 src/rules/line/strictMacroDefinition.spec.ts delete mode 100644 src/rules/line/strictMacroDefinition.ts diff --git a/src/rules/file/index.ts b/src/rules/file/index.ts index 40730af..b3a13b7 100644 --- a/src/rules/file/index.ts +++ b/src/rules/file/index.ts @@ -3,3 +3,4 @@ export { hasMacroNameInMend } from './hasMacroNameInMend' export { hasMacroParentheses } from './hasMacroParentheses' export { lineEndings } from './lineEndings' export { noNestedMacros } from './noNestedMacros' +export { strictMacroDefinition } from './strictMacroDefinition' diff --git a/src/rules/file/strictMacroDefinition.spec.ts b/src/rules/file/strictMacroDefinition.spec.ts new file mode 100644 index 0000000..ac7e982 --- /dev/null +++ b/src/rules/file/strictMacroDefinition.spec.ts @@ -0,0 +1,204 @@ +import { LintConfig, Severity } from '../../types' +import { strictMacroDefinition } from './strictMacroDefinition' + +describe('strictMacroDefinition', () => { + it('should return an empty array when the content has correct macro definition syntax', () => { + const content = '%macro somemacro;' + expect(strictMacroDefinition.test(content)).toEqual([]) + + const content2 = '%macro somemacro();' + expect(strictMacroDefinition.test(content2)).toEqual([]) + + const content3 = '%macro somemacro(var1);' + expect(strictMacroDefinition.test(content3)).toEqual([]) + + const content4 = '%macro somemacro/minoperator;' + expect(strictMacroDefinition.test(content4)).toEqual([]) + + const content5 = '%macro somemacro /minoperator;' + expect(strictMacroDefinition.test(content5)).toEqual([]) + + const content6 = '%macro somemacro(var1, var2)/minoperator;' + expect(strictMacroDefinition.test(content6)).toEqual([]) + + const content7 = + ' /* Some Comment */ %macro somemacro(var1, var2) /minoperator ; /* Some Comment */' + expect(strictMacroDefinition.test(content7)).toEqual([]) + + const content8 = + '%macro macroName( arr, arr/* / store source */3 ) /* / store source */;/* / store source */' + expect(strictMacroDefinition.test(content8)).toEqual([]) + + const content9 = '%macro macroName(var1, var2=with space, var3=);' + expect(strictMacroDefinition.test(content9)).toEqual([]) + + const content10 = '%macro macroName()/ /* some comment */ store source;' + expect(strictMacroDefinition.test(content10)).toEqual([]) + + const content11 = '`%macro macroName() /* / store source */;' + expect(strictMacroDefinition.test(content11)).toEqual([]) + }) + + it('should return an array with a single diagnostic when Macro definition has space in param', () => { + const content = '%macro somemacro(va r1);' + expect(strictMacroDefinition.test(content)).toEqual([ + { + message: `Param 'va r1' cannot have space`, + lineNumber: 1, + startColumnNumber: 18, + endColumnNumber: 22, + severity: Severity.Warning + } + ]) + }) + + it('should return an array with a two diagnostics when Macro definition has space in params', () => { + const content = '%macro somemacro(var1, var 2, v ar3, var4);' + expect(strictMacroDefinition.test(content)).toEqual([ + { + message: `Param 'var 2' cannot have space`, + lineNumber: 1, + startColumnNumber: 24, + endColumnNumber: 28, + severity: Severity.Warning + }, + { + message: `Param 'v ar3' cannot have space`, + lineNumber: 1, + startColumnNumber: 31, + endColumnNumber: 35, + severity: Severity.Warning + } + ]) + }) + + it('should return an array with a two diagnostics when Macro definition has space in params - special case', () => { + const content = + '%macro macroName( arr, ar r/* / store source */ 3 ) /* / store source */;/* / store source */' + expect(strictMacroDefinition.test(content)).toEqual([ + { + message: `Param 'ar r 3' cannot have space`, + lineNumber: 1, + startColumnNumber: 24, + endColumnNumber: 49, + severity: Severity.Warning + } + ]) + }) + + it('should return an array with a single diagnostic when Macro definition has invalid option', () => { + const content = '%macro somemacro(var1, var2)/minXoperator;' + expect(strictMacroDefinition.test(content)).toEqual([ + { + message: `Option 'minXoperator' is not valid`, + lineNumber: 1, + startColumnNumber: 30, + endColumnNumber: 41, + severity: Severity.Warning + } + ]) + }) + + it('should return an array with a two diagnostics when Macro definition has invalid options', () => { + const content = + '%macro somemacro(var1, var2)/ store invalidoption secure ;' + expect(strictMacroDefinition.test(content)).toEqual([ + { + message: `Option 'invalidoption' is not valid`, + lineNumber: 1, + startColumnNumber: 39, + endColumnNumber: 51, + severity: Severity.Warning + } + ]) + }) + + describe('multi-content macro declarations', () => { + it('should return an array with a single diagnostic when Macro definition has space in param', () => { + const content = `%macro + somemacro(va r1);` + expect(strictMacroDefinition.test(content)).toEqual([ + { + message: `Param 'va r1' cannot have space`, + lineNumber: 2, + startColumnNumber: 18, + endColumnNumber: 22, + severity: Severity.Warning + } + ]) + }) + + it('should return an array with a two diagnostics when Macro definition has space in params', () => { + const content = `%macro somemacro( + var1, + var 2, + v ar3, + var4);` + expect(strictMacroDefinition.test(content)).toEqual([ + { + message: `Param 'var 2' cannot have space`, + lineNumber: 3, + startColumnNumber: 7, + endColumnNumber: 11, + severity: Severity.Warning + }, + { + message: `Param 'v ar3' cannot have space`, + lineNumber: 4, + startColumnNumber: 7, + endColumnNumber: 11, + severity: Severity.Warning + } + ]) + }) + + it('should return an array with a two diagnostics when Macro definition has space in params - special case', () => { + const content = `%macro macroName( + arr, + ar r/* / store source */ 3 + ) /* / store source */;/* / store source */` + expect(strictMacroDefinition.test(content)).toEqual([ + { + message: `Param 'ar r 3' cannot have space`, + lineNumber: 3, + startColumnNumber: 7, + endColumnNumber: 32, + severity: Severity.Warning + } + ]) + }) + + it('should return an array with a single diagnostic when Macro definition has invalid option', () => { + const content = `%macro somemacro(var1, var2) + /minXoperator;` + expect(strictMacroDefinition.test(content)).toEqual([ + { + message: `Option 'minXoperator' is not valid`, + lineNumber: 2, + startColumnNumber: 8, + endColumnNumber: 19, + severity: Severity.Warning + } + ]) + }) + + it('should return an array with a two diagnostics when Macro definition has invalid options', () => { + const content = `%macro + somemacro( + var1, var2 + ) + / store + invalidoption + secure ;` + expect(strictMacroDefinition.test(content)).toEqual([ + { + message: `Option 'invalidoption' is not valid`, + lineNumber: 6, + startColumnNumber: 16, + endColumnNumber: 28, + severity: Severity.Warning + } + ]) + }) + }) +}) diff --git a/src/rules/file/strictMacroDefinition.ts b/src/rules/file/strictMacroDefinition.ts new file mode 100644 index 0000000..cc4db97 --- /dev/null +++ b/src/rules/file/strictMacroDefinition.ts @@ -0,0 +1,140 @@ +import { Diagnostic } from '../../types/Diagnostic' +import { LintConfig } from '../../types' +import { FileLintRule } from '../../types/LintRule' +import { LintRuleType } from '../../types/LintRuleType' +import { Severity } from '../../types/Severity' +import { parseMacros } from '../../utils/parseMacros' + +const name = 'strictMacroDefinition' +const description = 'Enforce strictly rules of macro definition syntax.' +const message = 'Incorrent Macro Definition Syntax' + +const validOptions = [ + 'CMD', + 'DES', + 'MINDELIMITER', + 'MINOPERATOR', + 'NOMINOPERATOR', + 'PARMBUFF', + 'SECURE', + 'NOSECURE', + 'STMT', + 'SOURCE', + 'SRC', + 'STORE' +] + +const test = (value: string, config?: LintConfig) => { + const diagnostics: Diagnostic[] = [] + + const macros = parseMacros(value, config) + + macros.forEach((macro) => { + const declaration = macro.declaration + + const regExpParams = new RegExp(/\((.*?)\)/) + const regExpParamsResult = regExpParams.exec(declaration) + + let _declaration = declaration + if (regExpParamsResult) { + const paramsPresent = regExpParamsResult[1] + + const paramsTrimmed = paramsPresent.trim() + const params = paramsTrimmed.split(',') + params.forEach((param) => { + const trimedParam = param.split('=')[0].trim() + + let paramLineNumber: number = 1, + paramStartIndex: number = 1, + paramEndIndex: number = value.length + + if ( + macro.declarationLines.findIndex( + (dl) => dl.indexOf(trimedParam) !== -1 + ) === -1 + ) { + const comment = '/\\*(.*?)\\*/' + for (let i = 1; i < trimedParam.length; i++) { + const paramWithComment = + trimedParam.slice(0, i) + comment + trimedParam.slice(i) + const regEx = new RegExp(paramWithComment) + + const declarationLineIndex = macro.declarationLines.findIndex( + (dl) => !!regEx.exec(dl) + ) + + if (declarationLineIndex !== -1) { + const declarationLine = + macro.declarationLines[declarationLineIndex] + const partFound = regEx.exec(declarationLine)![0] + + paramLineNumber = macro.startLineNumbers[declarationLineIndex] + paramStartIndex = declarationLine.indexOf(partFound) + paramEndIndex = + declarationLine.indexOf(partFound) + partFound.length + break + } + } + } else { + const declarationLineIndex = macro.declarationLines.findIndex( + (dl) => dl.indexOf(trimedParam) !== -1 + ) + const declarationLine = macro.declarationLines[declarationLineIndex] + paramLineNumber = macro.startLineNumbers[declarationLineIndex] + + paramStartIndex = declarationLine.indexOf(trimedParam) + paramEndIndex = + declarationLine.indexOf(trimedParam) + trimedParam.length + } + + if (trimedParam.includes(' ')) { + diagnostics.push({ + message: `Param '${trimedParam}' cannot have space`, + lineNumber: paramLineNumber, + startColumnNumber: paramStartIndex + 1, + endColumnNumber: paramEndIndex, + severity: Severity.Warning + }) + } + }) + + _declaration = declaration.split(`(${paramsPresent})`)[1] + } + + const optionsPresent = _declaration.split('/')?.[1]?.trim().split(' ') + + optionsPresent + ?.filter((o) => !!o) + .forEach((option) => { + const trimmedOption = option.trim() + if (!validOptions.includes(trimmedOption.toUpperCase())) { + const declarationLineIndex = macro.declarationLines.findIndex( + (dl) => dl.indexOf(trimmedOption) !== -1 + ) + const declarationLine = macro.declarationLines[declarationLineIndex] + + diagnostics.push({ + message: `Option '${trimmedOption}' is not valid`, + lineNumber: macro.startLineNumbers[declarationLineIndex], + startColumnNumber: declarationLine.indexOf(trimmedOption) + 1, + endColumnNumber: + declarationLine.indexOf(trimmedOption) + trimmedOption.length, + severity: Severity.Warning + }) + } + }) + }) + + return diagnostics +} + +/** + * Lint rule that checks if a line has followed syntax for macro definition + */ +export const strictMacroDefinition: FileLintRule = { + type: LintRuleType.File, + name, + description, + message, + test +} diff --git a/src/rules/line/index.ts b/src/rules/line/index.ts index 6029956..e8b0f70 100644 --- a/src/rules/line/index.ts +++ b/src/rules/line/index.ts @@ -3,4 +3,3 @@ export { maxLineLength } from './maxLineLength' export { noEncodedPasswords } from './noEncodedPasswords' export { noTabIndentation } from './noTabIndentation' export { noTrailingSpaces } from './noTrailingSpaces' -export { strictMacroDefinition } from './strictMacroDefinition' diff --git a/src/rules/line/strictMacroDefinition.spec.ts b/src/rules/line/strictMacroDefinition.spec.ts deleted file mode 100644 index 750084d..0000000 --- a/src/rules/line/strictMacroDefinition.spec.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { LintConfig, Severity } from '../../types' -import { strictMacroDefinition } from './strictMacroDefinition' - -describe('strictMacroDefinition', () => { - it('should return an empty array when the line has correct macro definition syntax', () => { - const line = '%macro somemacro;' - expect(strictMacroDefinition.test(line, 1)).toEqual([]) - - const line2 = '%macro somemacro();' - expect(strictMacroDefinition.test(line2, 1)).toEqual([]) - - const line3 = '%macro somemacro(var1);' - expect(strictMacroDefinition.test(line3, 1)).toEqual([]) - - const line4 = '%macro somemacro/minoperator;' - expect(strictMacroDefinition.test(line4, 1)).toEqual([]) - - const line5 = '%macro somemacro /minoperator;' - expect(strictMacroDefinition.test(line5, 1)).toEqual([]) - - const line6 = '%macro somemacro(var1, var2)/minoperator;' - expect(strictMacroDefinition.test(line6, 1)).toEqual([]) - - const line7 = - ' /* Some Comment */ %macro somemacro(var1, var2) /minoperator ; /* Some Comment */' - expect(strictMacroDefinition.test(line7, 1)).toEqual([]) - - const line8 = - '%macro macroName( arr, arr/* / store source */3 ) /* / store source */;/* / store source */' - expect(strictMacroDefinition.test(line8, 1)).toEqual([]) - - const line9 = '%macro macroName(var1, var2=with space, var3=);' - expect(strictMacroDefinition.test(line9, 1)).toEqual([]) - - const line10 = '%macro macroName()/ /* some comment */ store source;' - expect(strictMacroDefinition.test(line10, 1)).toEqual([]) - - const line11 = '`%macro macroName() /* / store source */;' - expect(strictMacroDefinition.test(line11, 1)).toEqual([]) - }) - - it('should return an array with a single diagnostic when Macro definition has space in param', () => { - const line = '%macro somemacro(va r1);' - expect(strictMacroDefinition.test(line, 1)).toEqual([ - { - message: `Param 'va r1' cannot have space`, - lineNumber: 1, - startColumnNumber: 18, - endColumnNumber: 22, - severity: Severity.Warning - } - ]) - }) - - it('should return an array with a two diagnostics when Macro definition has space in params', () => { - const line = '%macro somemacro(var1, var 2, v ar3, var4);' - expect(strictMacroDefinition.test(line, 1)).toEqual([ - { - message: `Param 'var 2' cannot have space`, - lineNumber: 1, - startColumnNumber: 24, - endColumnNumber: 28, - severity: Severity.Warning - }, - { - message: `Param 'v ar3' cannot have space`, - lineNumber: 1, - startColumnNumber: 31, - endColumnNumber: 35, - severity: Severity.Warning - } - ]) - }) - - it('should return an array with a two diagnostics when Macro definition has space in params - special case', () => { - const line = - '%macro macroName( arr, ar r/* / store source */ 3 ) /* / store source */;/* / store source */' - expect(strictMacroDefinition.test(line, 1)).toEqual([ - { - message: `Param 'ar r 3' cannot have space`, - lineNumber: 1, - startColumnNumber: 24, - endColumnNumber: 49, - severity: Severity.Warning - } - ]) - }) - - it('should return an array with a single diagnostic when Macro definition has invalid option', () => { - const line = '%macro somemacro(var1, var2)/minXoperator;' - expect(strictMacroDefinition.test(line, 1)).toEqual([ - { - message: `Option 'minXoperator' is not valid`, - lineNumber: 1, - startColumnNumber: 30, - endColumnNumber: 41, - severity: Severity.Warning - } - ]) - }) - - it('should return an array with a two diagnostics when Macro definition has invalid options', () => { - const line = - '%macro somemacro(var1, var2)/ store invalidoption secure ;' - expect(strictMacroDefinition.test(line, 1)).toEqual([ - { - message: `Option 'invalidoption' is not valid`, - lineNumber: 1, - startColumnNumber: 39, - endColumnNumber: 51, - severity: Severity.Warning - } - ]) - }) -}) diff --git a/src/rules/line/strictMacroDefinition.ts b/src/rules/line/strictMacroDefinition.ts deleted file mode 100644 index 246ba7f..0000000 --- a/src/rules/line/strictMacroDefinition.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Diagnostic } from '../../types/Diagnostic' -import { LintConfig } from '../../types' -import { LineLintRule } from '../../types/LintRule' -import { LintRuleType } from '../../types/LintRuleType' -import { Severity } from '../../types/Severity' -import { parseMacros } from '../../utils/parseMacros' - -const name = 'strictMacroDefinition' -const description = 'Enforce strictly rules of macro definition syntax.' -const message = 'Incorrent Macro Definition Syntax' - -const validOptions = [ - 'CMD', - 'DES', - 'MINDELIMITER', - 'MINOPERATOR', - 'NOMINOPERATOR', - 'PARMBUFF', - 'SECURE', - 'NOSECURE', - 'STMT', - 'SOURCE', - 'SRC', - 'STORE' -] - -const test = (value: string, lineNumber: number) => { - const diagnostics: Diagnostic[] = [] - - const macros = parseMacros(value) - const declaration = macros[0]?.declaration - if (!declaration) return [] - - const regExpParams = new RegExp(/\((.*?)\)/) - const regExpParamsResult = regExpParams.exec(declaration) - - let _declaration = declaration - if (regExpParamsResult) { - const paramsPresent = regExpParamsResult[1] - - const paramsTrimmed = paramsPresent.trim() - const params = paramsTrimmed.split(',') - params.forEach((param) => { - const trimedParam = param.split('=')[0].trim() - - let paramStartIndex: number = 1, - paramEndIndex: number = value.length - - if (value.indexOf(trimedParam) === -1) { - const comment = '/\\*(.*?)\\*/' - for (let i = 1; i < trimedParam.length; i++) { - const paramWithComment = - trimedParam.slice(0, i) + comment + trimedParam.slice(i) - const regEx = new RegExp(paramWithComment) - const result = regEx.exec(value) - if (result) { - paramStartIndex = value.indexOf(result[0]) - paramEndIndex = value.indexOf(result[0]) + result[0].length - break - } - } - } else { - paramStartIndex = value.indexOf(trimedParam) - paramEndIndex = value.indexOf(trimedParam) + trimedParam.length - } - - if (trimedParam.includes(' ')) { - diagnostics.push({ - message: `Param '${trimedParam}' cannot have space`, - lineNumber, - startColumnNumber: paramStartIndex + 1, - endColumnNumber: paramEndIndex, - severity: Severity.Warning - }) - } - }) - - _declaration = declaration.split(`(${paramsPresent})`)[1] - } - - const optionsPresent = _declaration.split('/')?.[1]?.trim().split(' ') - - optionsPresent - ?.filter((o) => !!o) - .forEach((option) => { - const trimmedOption = option.trim() - if (!validOptions.includes(trimmedOption.toUpperCase())) { - diagnostics.push({ - message: `Option '${trimmedOption}' is not valid`, - lineNumber, - startColumnNumber: value.indexOf(trimmedOption) + 1, - endColumnNumber: value.indexOf(trimmedOption) + trimmedOption.length, - severity: Severity.Warning - }) - } - }) - - return diagnostics -} - -/** - * Lint rule that checks if a line has followed syntax for macro definition - */ -export const strictMacroDefinition: LineLintRule = { - type: LintRuleType.Line, - name, - description, - message, - test -} diff --git a/src/types/LintConfig.ts b/src/types/LintConfig.ts index 1ecbd06..f8fe2d3 100644 --- a/src/types/LintConfig.ts +++ b/src/types/LintConfig.ts @@ -3,15 +3,15 @@ import { hasMacroNameInMend, noNestedMacros, hasMacroParentheses, - lineEndings + lineEndings, + strictMacroDefinition } from '../rules/file' import { indentationMultiple, maxLineLength, noEncodedPasswords, noTabIndentation, - noTrailingSpaces, - strictMacroDefinition + noTrailingSpaces } from '../rules/line' import { lowerCaseFileNames, noSpacesInFileNames } from '../rules/path' import { LineEndings } from './LineEndings' @@ -93,7 +93,7 @@ export class LintConfig { } if (json?.strictMacroDefinition) { - this.lineLintRules.push(strictMacroDefinition) + this.fileLintRules.push(strictMacroDefinition) } } } diff --git a/src/utils/getLintConfig.spec.ts b/src/utils/getLintConfig.spec.ts index 0374ca1..75be58d 100644 --- a/src/utils/getLintConfig.spec.ts +++ b/src/utils/getLintConfig.spec.ts @@ -2,8 +2,8 @@ import * as fileModule from '@sasjs/utils/file' import { LintConfig } from '../types/LintConfig' import { getLintConfig } from './getLintConfig' -const expectedFileLintRulesCount = 4 -const expectedLineLintRulesCount = 6 +const expectedFileLintRulesCount = 5 +const expectedLineLintRulesCount = 5 const expectedPathLintRulesCount = 2 describe('getLintConfig', () => { From d391a4e8fc8998e3d47c57bb1bd0a6620a2cd0a8 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Fri, 21 May 2021 18:05:22 +0500 Subject: [PATCH 05/11] fix(trimComments): handle case special comment case --- src/utils/trimComments.spec.ts | 12 ++++++++++++ src/utils/trimComments.ts | 10 +++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/utils/trimComments.spec.ts b/src/utils/trimComments.spec.ts index 1a1b169..6efaaff 100644 --- a/src/utils/trimComments.spec.ts +++ b/src/utils/trimComments.spec.ts @@ -8,6 +8,18 @@ describe('trimComments', () => { `) ).toEqual({ statement: 'some code;', commentStarted: false }) + expect( + trimComments(` + /*/ some comment */ some code; + `) + ).toEqual({ statement: 'some code;', commentStarted: false }) + + expect( + trimComments(` + some code;/*/ some comment */ some code; + `) + ).toEqual({ statement: 'some code; some code;', commentStarted: false }) + expect( trimComments(`/* some comment */ /* some comment */ CODE_Keyword1 /* some comment */ CODE_Keyword2/* some comment */;/* some comment */ diff --git a/src/utils/trimComments.ts b/src/utils/trimComments.ts index 6660435..0123919 100644 --- a/src/utils/trimComments.ts +++ b/src/utils/trimComments.ts @@ -6,7 +6,9 @@ export const trimComments = ( let trimmed = trimEnd ? (statement || '').trimEnd() : (statement || '').trim() if (commentStarted || trimmed.startsWith('/*')) { - const parts = trimmed.split('*/') + const parts = trimmed.startsWith('/*') + ? trimmed.slice(2).split('*/') + : trimmed.split('*/') if (parts.length === 2) { return { statement: (parts.pop() as string).trim(), @@ -20,10 +22,8 @@ export const trimComments = ( } } else if (trimmed.includes('/*')) { const statementBeforeCommentStarts = trimmed.slice(0, trimmed.indexOf('/*')) - const remainingStatement = trimmed.slice( - trimmed.indexOf('*/') + 2, - trimmed.length - ) + trimmed = trimmed.slice(trimmed.indexOf('/*') + 2) + const remainingStatement = trimmed.slice(trimmed.indexOf('*/') + 2) const result = trimComments(remainingStatement, false, true) const completeStatement = statementBeforeCommentStarts + result.statement From cbfa1f40d16ddefbce8275d36fed477e03160351 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Fri, 21 May 2021 18:32:22 +0500 Subject: [PATCH 06/11] fix(strictMacroDefinition): updated logic for getting params --- src/rules/file/strictMacroDefinition.spec.ts | 5 +++++ src/rules/file/strictMacroDefinition.ts | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/rules/file/strictMacroDefinition.spec.ts b/src/rules/file/strictMacroDefinition.spec.ts index ac7e982..1b62fb7 100644 --- a/src/rules/file/strictMacroDefinition.spec.ts +++ b/src/rules/file/strictMacroDefinition.spec.ts @@ -114,6 +114,11 @@ describe('strictMacroDefinition', () => { }) describe('multi-content macro declarations', () => { + it('should return an empty array when the content has correct macro definition syntax', () => { + const content = `%macro mp_ds2cards(base_ds=, tgt_ds=\n ,cards_file="%sysfunc(pathname(work))/cardgen.sas"\n ,maxobs=max\n ,random_sample=NO\n ,showlog=YES\n ,outencoding=\n ,append=NO\n)/*/STORE SOURCE*/;` + expect(strictMacroDefinition.test(content)).toEqual([]) + }) + it('should return an array with a single diagnostic when Macro definition has space in param', () => { const content = `%macro somemacro(va r1);` diff --git a/src/rules/file/strictMacroDefinition.ts b/src/rules/file/strictMacroDefinition.ts index cc4db97..b5954bf 100644 --- a/src/rules/file/strictMacroDefinition.ts +++ b/src/rules/file/strictMacroDefinition.ts @@ -32,12 +32,12 @@ const test = (value: string, config?: LintConfig) => { macros.forEach((macro) => { const declaration = macro.declaration - const regExpParams = new RegExp(/\((.*?)\)/) + const regExpParams = new RegExp(/(?<=\().*(?=\))/) const regExpParamsResult = regExpParams.exec(declaration) let _declaration = declaration if (regExpParamsResult) { - const paramsPresent = regExpParamsResult[1] + const paramsPresent = regExpParamsResult[0] const paramsTrimmed = paramsPresent.trim() const params = paramsTrimmed.split(',') From 5701064c07e12f4af347953d306a8860d630f9a9 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Fri, 21 May 2021 19:41:45 +0500 Subject: [PATCH 07/11] fix(strictMacroDefinition): updated logic for getting options --- src/rules/file/strictMacroDefinition.spec.ts | 4 ++ src/rules/file/strictMacroDefinition.ts | 55 +++++++++++++------- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/rules/file/strictMacroDefinition.spec.ts b/src/rules/file/strictMacroDefinition.spec.ts index 1b62fb7..1e757bf 100644 --- a/src/rules/file/strictMacroDefinition.spec.ts +++ b/src/rules/file/strictMacroDefinition.spec.ts @@ -37,6 +37,10 @@ describe('strictMacroDefinition', () => { const content11 = '`%macro macroName() /* / store source */;' expect(strictMacroDefinition.test(content11)).toEqual([]) + + const content12 = + '%macro macroName()/ /* some comment */ store des="some description";' + expect(strictMacroDefinition.test(content12)).toEqual([]) }) it('should return an array with a single diagnostic when Macro definition has space in param', () => { diff --git a/src/rules/file/strictMacroDefinition.ts b/src/rules/file/strictMacroDefinition.ts index b5954bf..b55eaf0 100644 --- a/src/rules/file/strictMacroDefinition.ts +++ b/src/rules/file/strictMacroDefinition.ts @@ -101,28 +101,43 @@ const test = (value: string, config?: LintConfig) => { _declaration = declaration.split(`(${paramsPresent})`)[1] } - const optionsPresent = _declaration.split('/')?.[1]?.trim().split(' ') + let optionsPresent = _declaration.split('/')?.[1]?.trim() - optionsPresent - ?.filter((o) => !!o) - .forEach((option) => { - const trimmedOption = option.trim() - if (!validOptions.includes(trimmedOption.toUpperCase())) { - const declarationLineIndex = macro.declarationLines.findIndex( - (dl) => dl.indexOf(trimmedOption) !== -1 - ) - const declarationLine = macro.declarationLines[declarationLineIndex] + if (optionsPresent) { + const regex = new RegExp(/="(.*?)"/, 'g') - diagnostics.push({ - message: `Option '${trimmedOption}' is not valid`, - lineNumber: macro.startLineNumbers[declarationLineIndex], - startColumnNumber: declarationLine.indexOf(trimmedOption) + 1, - endColumnNumber: - declarationLine.indexOf(trimmedOption) + trimmedOption.length, - severity: Severity.Warning - }) - } - }) + let result = regex.exec(optionsPresent) + + while (result) { + optionsPresent = + optionsPresent.slice(0, result.index) + + optionsPresent.slice(result.index + result[0].length) + + result = regex.exec(optionsPresent) + } + + optionsPresent + .split(' ') + ?.filter((o) => !!o) + .forEach((option) => { + const trimmedOption = option.trim() + if (!validOptions.includes(trimmedOption.toUpperCase())) { + const declarationLineIndex = macro.declarationLines.findIndex( + (dl) => dl.indexOf(trimmedOption) !== -1 + ) + const declarationLine = macro.declarationLines[declarationLineIndex] + + diagnostics.push({ + message: `Option '${trimmedOption}' is not valid`, + lineNumber: macro.startLineNumbers[declarationLineIndex], + startColumnNumber: declarationLine.indexOf(trimmedOption) + 1, + endColumnNumber: + declarationLine.indexOf(trimmedOption) + trimmedOption.length, + severity: Severity.Warning + }) + } + }) + } }) return diagnostics From c9fa36613042828350d453c56a91ed3b523b1d5d Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Fri, 21 May 2021 20:05:53 +0500 Subject: [PATCH 08/11] fix(parseMacros): avoid statement break on html encoded semi colon --- src/rules/file/strictMacroDefinition.spec.ts | 3 ++ src/utils/parseMacros.spec.ts | 33 ++++++++++++++++++++ src/utils/parseMacros.ts | 13 ++++++++ 3 files changed, 49 insertions(+) diff --git a/src/rules/file/strictMacroDefinition.spec.ts b/src/rules/file/strictMacroDefinition.spec.ts index 1e757bf..0e5b50e 100644 --- a/src/rules/file/strictMacroDefinition.spec.ts +++ b/src/rules/file/strictMacroDefinition.spec.ts @@ -121,6 +121,9 @@ describe('strictMacroDefinition', () => { it('should return an empty array when the content has correct macro definition syntax', () => { const content = `%macro mp_ds2cards(base_ds=, tgt_ds=\n ,cards_file="%sysfunc(pathname(work))/cardgen.sas"\n ,maxobs=max\n ,random_sample=NO\n ,showlog=YES\n ,outencoding=\n ,append=NO\n)/*/STORE SOURCE*/;` expect(strictMacroDefinition.test(content)).toEqual([]) + + const content2 = `%macro mm_createapplication(\n tree=/User Folders/sasdemo\n ,name=myApp\n ,ClassIdentifier=mcore\n ,desc=Created by mm_createapplication\n ,params= param1=1 param2=blah\n ,version=\n ,frefin=mm_in\n ,frefout=mm_out\n ,mDebug=1\n );` + expect(strictMacroDefinition.test(content2)).toEqual([]) }) it('should return an array with a single diagnostic when Macro definition has space in param', () => { diff --git a/src/utils/parseMacros.spec.ts b/src/utils/parseMacros.spec.ts index e8f25ce..a7b2052 100644 --- a/src/utils/parseMacros.spec.ts +++ b/src/utils/parseMacros.spec.ts @@ -277,5 +277,38 @@ describe('parseMacros', () => { mismatchedMendMacroName: '' }) }) + + it('should return an array with a single macro having semi-colon in params', () => { + const text = `\n%macro mm_createapplication(\n tree=/User Folders/sasdemo\n ,name=myApp\n ,ClassIdentifier=mcore\n ,desc=Created by mm_createapplication\n ,params= param1=1 param2=blah\n ,version=\n ,frefin=mm_in\n ,frefout=mm_out\n ,mDebug=1\n );` + + const macros = parseMacros(text, new LintConfig()) + + expect(macros.length).toEqual(1) + expect(macros).toContainEqual({ + name: 'mm_createapplication', + declarationLines: [ + `%macro mm_createapplication(`, + ` tree=/User Folders/sasdemo`, + ` ,name=myApp`, + ` ,ClassIdentifier=mcore`, + ` ,desc=Created by mm_createapplication`, + ` ,params= param1=1 param2=blah`, + ` ,version=`, + ` ,frefin=mm_in`, + ` ,frefout=mm_out`, + ` ,mDebug=1`, + ` );` + ], + terminationLine: '', + declaration: + '%macro mm_createapplication( tree=/User Folders/sasdemo ,name=myApp ,ClassIdentifier=mcore ,desc=Created by mm_createapplication ,params= param1=1 param2=blah ,version= ,frefin=mm_in ,frefout=mm_out ,mDebug=1 )', + termination: '', + startLineNumbers: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + endLineNumber: null, + parentMacro: '', + hasMacroNameInMend: false, + mismatchedMendMacroName: '' + }) + }) }) }) diff --git a/src/utils/parseMacros.ts b/src/utils/parseMacros.ts index 591dc50..51a955e 100644 --- a/src/utils/parseMacros.ts +++ b/src/utils/parseMacros.ts @@ -38,6 +38,19 @@ export const parseMacros = (text: string, config?: LintConfig): Macro[] => { const statements: string[] = trimmedLine.split(';') + if (isReadingMacroDefinition) { + statements.forEach((statement, statementIndex) => { + if (/&[^\s]{1,5}$/.test(statement)) { + const next = statements[statementIndex] + const updatedStatement = `${statement};${ + statements[statementIndex + 1] + }` + statements.splice(statementIndex, 1, updatedStatement) + statements.splice(statementIndex + 1, 1) + } + }) + } + statements.forEach((statement, statementIndex) => { const { statement: trimmedStatement, commentStarted } = trimComments( statement, From a762dadf37e48281ec486c2309370aad4b2b1c9d Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Fri, 21 May 2021 20:08:38 +0500 Subject: [PATCH 09/11] chore: testing content into single line --- src/utils/parseMacros.spec.ts | 55 +++++++---------------------------- 1 file changed, 10 insertions(+), 45 deletions(-) diff --git a/src/utils/parseMacros.spec.ts b/src/utils/parseMacros.spec.ts index a7b2052..d438b75 100644 --- a/src/utils/parseMacros.spec.ts +++ b/src/utils/parseMacros.spec.ts @@ -3,9 +3,7 @@ import { parseMacros } from './parseMacros' describe('parseMacros', () => { it('should return an array with a single macro', () => { - const text = ` %macro test; - %put 'hello'; -%mend` + const text = ` %macro test;\n %put 'hello';\n%mend` const macros = parseMacros(text, new LintConfig()) @@ -25,9 +23,7 @@ describe('parseMacros', () => { }) it('should return an array with a single macro having parameters', () => { - const text = `%macro test(var,sum); - %put 'hello'; -%mend` + const text = `%macro test(var,sum);\n %put 'hello';\n%mend` const macros = parseMacros(text, new LintConfig()) @@ -47,9 +43,7 @@ describe('parseMacros', () => { }) it('should return an array with a single macro having PARMBUFF option', () => { - const text = `%macro test/parmbuff; - %put 'hello'; -%mend` + const text = `%macro test/parmbuff;\n %put 'hello';\n%mend` const macros = parseMacros(text, new LintConfig()) @@ -69,10 +63,7 @@ describe('parseMacros', () => { }) it('should return an array with a single macro having paramerter & SOURCE option', () => { - const text = `/* commentary */ %macro foobar(arg) /store source - des="This macro does not do much"; - %put 'hello'; -%mend` + const text = `/* commentary */ %macro foobar(arg) /store source\n des="This macro does not do much";\n %put 'hello';\n%mend` const macros = parseMacros(text, new LintConfig()) @@ -96,12 +87,7 @@ describe('parseMacros', () => { }) it('should return an array with multiple macros', () => { - const text = `%macro foo; - %put 'foo'; -%mend; -%macro bar(); - %put 'bar'; -%mend bar;` + const text = `%macro foo;\n %put 'foo';\n%mend;\n%macro bar();\n %put 'bar';\n%mend bar;` const macros = parseMacros(text, new LintConfig()) @@ -133,12 +119,7 @@ describe('parseMacros', () => { }) it('should detect nested macro definitions', () => { - const text = `%macro test(); - %put 'hello'; - %macro test2; - %put 'world; - %mend -%mend test` + const text = `%macro test();\n %put 'hello';\n %macro test2;\n %put 'world;\n %mend\n%mend test` const macros = parseMacros(text, new LintConfig()) @@ -171,10 +152,7 @@ describe('parseMacros', () => { describe(`multi-line macro declarations`, () => { it('should return an array with a single macro', () => { - const text = `%macro - test; - %put 'hello'; -%mend` + const text = `%macro \n test;\n %put 'hello';\n%mend` const macros = parseMacros(text, new LintConfig()) @@ -194,11 +172,7 @@ describe('parseMacros', () => { }) it('should return an array with a single macro having parameters', () => { - const text = `%macro - test( - var, - sum);%put 'hello'; -%mend` + const text = `%macro \n test(\n var,\n sum);%put 'hello';\n%mend` const macros = parseMacros(text, new LintConfig()) @@ -223,10 +197,7 @@ describe('parseMacros', () => { }) it('should return an array with a single macro having PARMBUFF option', () => { - const text = `%macro test - /parmbuff; - %put 'hello'; -%mend` + const text = `%macro test\n /parmbuff;\n %put 'hello';\n%mend` const macros = parseMacros(text, new LintConfig()) @@ -246,13 +217,7 @@ describe('parseMacros', () => { }) it('should return an array with a single macro having paramerter & SOURCE option', () => { - const text = `/* commentary */ %macro foobar/* commentary */(arg) - /* commentary */ - /store - /* commentary */source - des="This macro does not do much"; - %put 'hello'; -%mend` + const text = `/* commentary */ %macro foobar/* commentary */(arg) \n /* commentary */\n /store\n /* commentary */source\n des="This macro does not do much";\n %put 'hello';\n%mend` const macros = parseMacros(text, new LintConfig()) From 020a1e08d0f1180092a9021a9013f8c9cc0870c5 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Fri, 21 May 2021 20:16:24 +0500 Subject: [PATCH 10/11] chore: prevent space b/w macroName and starting parentheses is not applicable --- src/rules/file/hasMacroParentheses.spec.ts | 14 -------------- src/rules/file/hasMacroParentheses.ts | 12 ------------ 2 files changed, 26 deletions(-) diff --git a/src/rules/file/hasMacroParentheses.spec.ts b/src/rules/file/hasMacroParentheses.spec.ts index 3ace5ce..3bd7b2c 100644 --- a/src/rules/file/hasMacroParentheses.spec.ts +++ b/src/rules/file/hasMacroParentheses.spec.ts @@ -139,18 +139,4 @@ describe('hasMacroParentheses', () => { ]) }) }) - - // it('should return an array with a single diagnostic when a macro definition contains a space', () => { - // const content = `%macro test ()` - - // expect(hasMacroParentheses.test(content)).toEqual([ - // { - // message: 'Macro definition contains space(s)', - // lineNumber: 1, - // startColumnNumber: 8, - // endColumnNumber: 14, - // severity: Severity.Warning - // } - // ]) - // }) }) diff --git a/src/rules/file/hasMacroParentheses.ts b/src/rules/file/hasMacroParentheses.ts index 13d15e5..435d98d 100644 --- a/src/rules/file/hasMacroParentheses.ts +++ b/src/rules/file/hasMacroParentheses.ts @@ -46,18 +46,6 @@ const test = (value: string, config?: LintConfig) => { 1, severity: Severity.Warning }) - // } else if (macro.name !== macro.name.trim()) { - // diagnostics.push({ - // message: 'Macro definition contains space(s)', - // lineNumber: macro.startLineNumber!, - // startColumnNumber: getColumnNumber(macro.declarationLine, macro.name), - // endColumnNumber: - // getColumnNumber(macro.declarationLine, macro.name) + - // macro.name.length - - // 1 + - // `()`.length, - // severity: Severity.Warning - // }) } }) From 0caf31b7ffb95884f92997d9dc95dc7a4af1dd4a Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Sat, 22 May 2021 00:08:24 +0500 Subject: [PATCH 11/11] chore: Code Refactor --- src/rules/file/strictMacroDefinition.ts | 216 +++++++++++++----------- src/types/Macro.ts | 12 ++ src/types/index.ts | 1 + src/utils/parseMacros.ts | 17 +- 4 files changed, 131 insertions(+), 115 deletions(-) create mode 100644 src/types/Macro.ts diff --git a/src/rules/file/strictMacroDefinition.ts b/src/rules/file/strictMacroDefinition.ts index b55eaf0..fed6b85 100644 --- a/src/rules/file/strictMacroDefinition.ts +++ b/src/rules/file/strictMacroDefinition.ts @@ -1,8 +1,6 @@ -import { Diagnostic } from '../../types/Diagnostic' -import { LintConfig } from '../../types' +import { Diagnostic, LintConfig, Macro, Severity } from '../../types' import { FileLintRule } from '../../types/LintRule' import { LintRuleType } from '../../types/LintRuleType' -import { Severity } from '../../types/Severity' import { parseMacros } from '../../utils/parseMacros' const name = 'strictMacroDefinition' @@ -24,120 +22,136 @@ const validOptions = [ 'STORE' ] -const test = (value: string, config?: LintConfig) => { - const diagnostics: Diagnostic[] = [] +const processParams = ( + content: string, + macro: Macro, + diagnostics: Diagnostic[] +): string => { + const declaration = macro.declaration + + const regExpParams = new RegExp(/(?<=\().*(?=\))/) + const regExpParamsResult = regExpParams.exec(declaration) + + let _declaration = declaration + if (regExpParamsResult) { + const paramsPresent = regExpParamsResult[0] + + const params = paramsPresent.trim().split(',') + params.forEach((param) => { + const trimedParam = param.split('=')[0].trim() + + let paramLineNumber: number = 1, + paramStartIndex: number = 1, + paramEndIndex: number = content.length + + if ( + macro.declarationLines.findIndex( + (dl) => dl.indexOf(trimedParam) !== -1 + ) === -1 + ) { + const comment = '/\\*(.*?)\\*/' + for (let i = 1; i < trimedParam.length; i++) { + const paramWithComment = + trimedParam.slice(0, i) + comment + trimedParam.slice(i) + const regEx = new RegExp(paramWithComment) - const macros = parseMacros(value, config) + const declarationLineIndex = macro.declarationLines.findIndex( + (dl) => !!regEx.exec(dl) + ) - macros.forEach((macro) => { - const declaration = macro.declaration - - const regExpParams = new RegExp(/(?<=\().*(?=\))/) - const regExpParamsResult = regExpParams.exec(declaration) - - let _declaration = declaration - if (regExpParamsResult) { - const paramsPresent = regExpParamsResult[0] - - const paramsTrimmed = paramsPresent.trim() - const params = paramsTrimmed.split(',') - params.forEach((param) => { - const trimedParam = param.split('=')[0].trim() - - let paramLineNumber: number = 1, - paramStartIndex: number = 1, - paramEndIndex: number = value.length - - if ( - macro.declarationLines.findIndex( - (dl) => dl.indexOf(trimedParam) !== -1 - ) === -1 - ) { - const comment = '/\\*(.*?)\\*/' - for (let i = 1; i < trimedParam.length; i++) { - const paramWithComment = - trimedParam.slice(0, i) + comment + trimedParam.slice(i) - const regEx = new RegExp(paramWithComment) - - const declarationLineIndex = macro.declarationLines.findIndex( - (dl) => !!regEx.exec(dl) - ) - - if (declarationLineIndex !== -1) { - const declarationLine = - macro.declarationLines[declarationLineIndex] - const partFound = regEx.exec(declarationLine)![0] - - paramLineNumber = macro.startLineNumbers[declarationLineIndex] - paramStartIndex = declarationLine.indexOf(partFound) - paramEndIndex = - declarationLine.indexOf(partFound) + partFound.length - break - } + if (declarationLineIndex !== -1) { + const declarationLine = macro.declarationLines[declarationLineIndex] + const partFound = regEx.exec(declarationLine)![0] + + paramLineNumber = macro.startLineNumbers[declarationLineIndex] + paramStartIndex = declarationLine.indexOf(partFound) + paramEndIndex = + declarationLine.indexOf(partFound) + partFound.length + break } - } else { + } + } else { + const declarationLineIndex = macro.declarationLines.findIndex( + (dl) => dl.indexOf(trimedParam) !== -1 + ) + const declarationLine = macro.declarationLines[declarationLineIndex] + paramLineNumber = macro.startLineNumbers[declarationLineIndex] + + paramStartIndex = declarationLine.indexOf(trimedParam) + paramEndIndex = + declarationLine.indexOf(trimedParam) + trimedParam.length + } + + if (trimedParam.includes(' ')) { + diagnostics.push({ + message: `Param '${trimedParam}' cannot have space`, + lineNumber: paramLineNumber, + startColumnNumber: paramStartIndex + 1, + endColumnNumber: paramEndIndex, + severity: Severity.Warning + }) + } + }) + + _declaration = declaration.split(`(${paramsPresent})`)[1] + } + return _declaration +} + +const processOptions = ( + _declaration: string, + macro: Macro, + diagnostics: Diagnostic[] +): void => { + let optionsPresent = _declaration.split('/')?.[1]?.trim() + + if (optionsPresent) { + const regex = new RegExp(/="(.*?)"/, 'g') + + let result = regex.exec(optionsPresent) + + // removing Option's `="..."` part, e.g. des="..." + while (result) { + optionsPresent = + optionsPresent.slice(0, result.index) + + optionsPresent.slice(result.index + result[0].length) + + result = regex.exec(optionsPresent) + } + + optionsPresent + .split(' ') + ?.filter((o) => !!o) + .forEach((option) => { + const trimmedOption = option.trim() + if (!validOptions.includes(trimmedOption.toUpperCase())) { const declarationLineIndex = macro.declarationLines.findIndex( - (dl) => dl.indexOf(trimedParam) !== -1 + (dl) => dl.indexOf(trimmedOption) !== -1 ) const declarationLine = macro.declarationLines[declarationLineIndex] - paramLineNumber = macro.startLineNumbers[declarationLineIndex] - paramStartIndex = declarationLine.indexOf(trimedParam) - paramEndIndex = - declarationLine.indexOf(trimedParam) + trimedParam.length - } - - if (trimedParam.includes(' ')) { diagnostics.push({ - message: `Param '${trimedParam}' cannot have space`, - lineNumber: paramLineNumber, - startColumnNumber: paramStartIndex + 1, - endColumnNumber: paramEndIndex, + message: `Option '${trimmedOption}' is not valid`, + lineNumber: macro.startLineNumbers[declarationLineIndex], + startColumnNumber: declarationLine.indexOf(trimmedOption) + 1, + endColumnNumber: + declarationLine.indexOf(trimmedOption) + trimmedOption.length, severity: Severity.Warning }) } }) + } +} - _declaration = declaration.split(`(${paramsPresent})`)[1] - } - - let optionsPresent = _declaration.split('/')?.[1]?.trim() - - if (optionsPresent) { - const regex = new RegExp(/="(.*?)"/, 'g') - - let result = regex.exec(optionsPresent) - - while (result) { - optionsPresent = - optionsPresent.slice(0, result.index) + - optionsPresent.slice(result.index + result[0].length) +const test = (value: string, config?: LintConfig) => { + const diagnostics: Diagnostic[] = [] - result = regex.exec(optionsPresent) - } + const macros = parseMacros(value, config) - optionsPresent - .split(' ') - ?.filter((o) => !!o) - .forEach((option) => { - const trimmedOption = option.trim() - if (!validOptions.includes(trimmedOption.toUpperCase())) { - const declarationLineIndex = macro.declarationLines.findIndex( - (dl) => dl.indexOf(trimmedOption) !== -1 - ) - const declarationLine = macro.declarationLines[declarationLineIndex] + macros.forEach((macro) => { + const _declaration = processParams(value, macro, diagnostics) - diagnostics.push({ - message: `Option '${trimmedOption}' is not valid`, - lineNumber: macro.startLineNumbers[declarationLineIndex], - startColumnNumber: declarationLine.indexOf(trimmedOption) + 1, - endColumnNumber: - declarationLine.indexOf(trimmedOption) + trimmedOption.length, - severity: Severity.Warning - }) - } - }) - } + processOptions(_declaration, macro, diagnostics) }) return diagnostics diff --git a/src/types/Macro.ts b/src/types/Macro.ts new file mode 100644 index 0000000..9a44866 --- /dev/null +++ b/src/types/Macro.ts @@ -0,0 +1,12 @@ +export interface Macro { + name: string + startLineNumbers: number[] + endLineNumber: number | null + declarationLines: string[] + terminationLine: string + declaration: string + termination: string + parentMacro: string + hasMacroNameInMend: boolean + mismatchedMendMacroName: string +} diff --git a/src/types/index.ts b/src/types/index.ts index aba5333..37b9911 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,3 +4,4 @@ export * from './LintConfig' export * from './LintRule' export * from './LintRuleType' export * from './Severity' +export * from './Macro' diff --git a/src/utils/parseMacros.ts b/src/utils/parseMacros.ts index 51a955e..2147e53 100644 --- a/src/utils/parseMacros.ts +++ b/src/utils/parseMacros.ts @@ -1,20 +1,7 @@ -import { LintConfig } from '../types/LintConfig' +import { LintConfig, Macro } from '../types' import { LineEndings } from '../types/LineEndings' import { trimComments } from './trimComments' -interface Macro { - name: string - startLineNumbers: number[] - endLineNumber: number | null - declarationLines: string[] - terminationLine: string - declaration: string - termination: string - parentMacro: string - hasMacroNameInMend: boolean - mismatchedMendMacroName: string -} - export const parseMacros = (text: string, config?: LintConfig): Macro[] => { const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n' const lines: string[] = text ? text.split(lineEnding) : [] @@ -39,6 +26,8 @@ export const parseMacros = (text: string, config?: LintConfig): Macro[] => { const statements: string[] = trimmedLine.split(';') if (isReadingMacroDefinition) { + // checking if code is split into statements based on `;` is a part of HTML Encoded Character + // if it happened, merges two statements into one statements.forEach((statement, statementIndex) => { if (/&[^\s]{1,5}$/.test(statement)) { const next = statements[statementIndex]