diff --git a/.sasjslint b/.sasjslint index 6fbb183..2d6a33f 100644 --- a/.sasjslint +++ b/.sasjslint @@ -7,8 +7,7 @@ "lowerCaseFileNames": true, "noTabIndentation": true, "indentationMultiple": 2, - "hasMacroNameInMend": true, + "hasMacroNameInMend": false, "noNestedMacros": true, - "hasMacroParentheses": true, - "lineEndings": "lf" + "hasMacroParentheses": true } \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 018b0ea..cecb39e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -9,5 +9,5 @@ module.exports = { statements: -10 } }, - collectCoverageFrom: ['src/**/{!(index|formatExample|lintExample),}.ts'] + collectCoverageFrom: ['src/**/{!(index|example),}.ts'] } diff --git a/src/lintExample.ts b/src/example.ts similarity index 96% rename from src/lintExample.ts rename to src/example.ts index d90808a..bae4de8 100644 --- a/src/lintExample.ts +++ b/src/example.ts @@ -1,58 +1,58 @@ -import { lintFile, lintText } from './lint' -import path from 'path' - -/** - * Example which tests a piece of text with all known violations. - */ - -const text = `/** - @file - @brief Returns an unused libref - @details Use as follows: - - libname mclib0 (work); - libname mclib1 (work); - libname mclib2 (work); - - %let libref=%mf_getuniquelibref({SAS001}); - %put &=libref; - - which returns: - - > mclib3 - - @param prefix= first part of libref. Remember that librefs can only be 8 characters, - so a 7 letter prefix would mean that maxtries should be 10. - @param maxtries= the last part of the libref. Provide an integer value. - - @version 9.2 - @author Allan Bowe - **/ - - -%macro mf_getuniquelibref(prefix=mclib,maxtries=1000); - %local x libref; - %let x={SAS002}; - %do x=0 %to &maxtries; - %if %sysfunc(libref(&prefix&x)) ne 0 %then %do; - %let libref=&prefix&x; - %let rc=%sysfunc(libname(&libref,%sysfunc(pathname(work)))); - %if &rc %then %put %sysfunc(sysmsg()); - &prefix&x - %*put &sysmacroname: Libref &libref assigned as WORK and returned; - %return; - %end; - %end; - %put unable to find available libref in range &prefix.0-&maxtries; - %mend; -` - -lintText(text).then((diagnostics) => { - console.log('Text lint results:') - console.table(diagnostics) -}) - -lintFile(path.join(__dirname, 'Example File.sas')).then((diagnostics) => { - console.log('File lint results:') - console.table(diagnostics) -}) +import { lintFile, lintText } from './lint' +import path from 'path' + +/** + * Example which tests a piece of text with all known violations. + */ + +const text = `/** + @file + @brief Returns an unused libref + @details Use as follows: + + libname mclib0 (work); + libname mclib1 (work); + libname mclib2 (work); + + %let libref=%mf_getuniquelibref({SAS001}); + %put &=libref; + + which returns: + + > mclib3 + + @param prefix= first part of libref. Remember that librefs can only be 8 characters, + so a 7 letter prefix would mean that maxtries should be 10. + @param maxtries= the last part of the libref. Provide an integer value. + + @version 9.2 + @author Allan Bowe + **/ + + +%macro mf_getuniquelibref(prefix=mclib,maxtries=1000); + %local x libref; + %let x={SAS002}; + %do x=0 %to &maxtries; + %if %sysfunc(libref(&prefix&x)) ne 0 %then %do; + %let libref=&prefix&x; + %let rc=%sysfunc(libname(&libref,%sysfunc(pathname(work)))); + %if &rc %then %put %sysfunc(sysmsg()); + &prefix&x + %*put &sysmacroname: Libref &libref assigned as WORK and returned; + %return; + %end; + %end; + %put unable to find available libref in range &prefix.0-&maxtries; + %mend; +` + +lintText(text).then((diagnostics) => { + console.log('Text lint results:') + console.table(diagnostics) +}) + +lintFile(path.join(__dirname, 'Example File.sas')).then((diagnostics) => { + console.log('File lint results:') + console.table(diagnostics) +}) diff --git a/src/format.ts b/src/format.ts new file mode 100644 index 0000000..96d852f --- /dev/null +++ b/src/format.ts @@ -0,0 +1 @@ +export const format = (text: string) => {} diff --git a/src/format/formatText.spec.ts b/src/format/formatText.spec.ts deleted file mode 100644 index 26db93d..0000000 --- a/src/format/formatText.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { formatText } from './formatText' -import * as getLintConfigModule from '../utils/getLintConfig' -import { LintConfig } from '../types' -jest.mock('../utils/getLintConfig') - -describe('formatText', () => { - it('should format the given text based on configured rules', async () => { - jest - .spyOn(getLintConfigModule, 'getLintConfig') - .mockImplementationOnce(() => - Promise.resolve( - new LintConfig(getLintConfigModule.DefaultLintConfiguration) - ) - ) - const text = `%macro test - %put 'hello';\r\n%mend; ` - - const expectedOutput = `/** - @file - @brief -**/\n%macro test - %put 'hello';\n%mend;` - - const output = await formatText(text) - - expect(output).toEqual(expectedOutput) - }) - - it('should use CRLF line endings when configured', async () => { - jest - .spyOn(getLintConfigModule, 'getLintConfig') - .mockImplementationOnce(() => - Promise.resolve( - new LintConfig({ - ...getLintConfigModule.DefaultLintConfiguration, - lineEndings: 'crlf' - }) - ) - ) - const text = `%macro test\n %put 'hello';\r\n%mend; ` - - const expectedOutput = `/**\r\n @file\r\n @brief \r\n**/\r\n%macro test\r\n %put 'hello';\r\n%mend;` - - const output = await formatText(text) - - expect(output).toEqual(expectedOutput) - }) -}) diff --git a/src/format/formatText.ts b/src/format/formatText.ts deleted file mode 100644 index b33807e..0000000 --- a/src/format/formatText.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { getLintConfig } from '../utils' -import { processText } from './shared' - -export const formatText = async (text: string) => { - const config = await getLintConfig() - return processText(text, config) -} diff --git a/src/format/shared.ts b/src/format/shared.ts deleted file mode 100644 index fa0ff02..0000000 --- a/src/format/shared.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { LintConfig } from '../types' -import { LineEndings } from '../types/LineEndings' -import { splitText } from '../utils/splitText' - -export const processText = (text: string, config: LintConfig) => { - const processedText = processContent(config, text) - const lines = splitText(processedText, config) - const formattedLines = lines.map((line) => { - return processLine(config, line) - }) - - const configuredLineEnding = - config.lineEndings === LineEndings.LF ? '\n' : '\r\n' - return formattedLines.join(configuredLineEnding) -} - -const processContent = (config: LintConfig, content: string): string => { - let processedContent = content - config.fileLintRules - .filter((r) => !!r.fix) - .forEach((rule) => { - processedContent = rule.fix!(processedContent) - }) - - return processedContent -} - -export const processLine = (config: LintConfig, line: string): string => { - let processedLine = line - config.lineLintRules - .filter((r) => !!r.fix) - .forEach((rule) => { - processedLine = rule.fix!(line) - }) - - return processedLine -} diff --git a/src/formatExample.ts b/src/formatExample.ts deleted file mode 100644 index 10a428c..0000000 --- a/src/formatExample.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { formatText } from './format/formatText' -import { lintText } from './lint' - -const content = `%put 'Hello'; -%put 'World'; -%macro somemacro() - %put 'test'; -%mend;\r\n` - -console.log(content) -lintText(content).then((diagnostics) => { - console.log('Before Formatting:') - console.table(diagnostics) - formatText(content).then((formattedText) => { - lintText(formattedText).then((newDiagnostics) => { - console.log('After Formatting:') - console.log(formattedText) - console.table(newDiagnostics) - }) - }) -}) diff --git a/src/lint/lintProject.spec.ts b/src/lint/lintProject.spec.ts index ec7245f..47e09d9 100644 --- a/src/lint/lintProject.spec.ts +++ b/src/lint/lintProject.spec.ts @@ -1,8 +1,8 @@ import { lintProject } from './lintProject' import { Severity } from '../types/Severity' -import * as getProjectRootModule from '../utils/getProjectRoot' +import * as utils from '../utils' import path from 'path' -jest.mock('../utils/getProjectRoot') +jest.mock('../utils') const expectedFilesCount = 1 const expectedDiagnostics = [ @@ -74,8 +74,8 @@ const expectedDiagnostics = [ describe('lintProject', () => { it('should identify lint issues in a given project', async () => { jest - .spyOn(getProjectRootModule, 'getProjectRoot') - .mockImplementation(() => Promise.resolve(path.join(__dirname, '..'))) + .spyOn(utils, 'getProjectRoot') + .mockImplementationOnce(() => Promise.resolve(path.join(__dirname, '..'))) const results = await lintProject() expect(results.size).toEqual(expectedFilesCount) @@ -96,7 +96,7 @@ describe('lintProject', () => { it('should throw an error when a project root is not found', async () => { jest - .spyOn(getProjectRootModule, 'getProjectRoot') + .spyOn(utils, 'getProjectRoot') .mockImplementationOnce(() => Promise.resolve('')) await expect(lintProject()).rejects.toThrowError( diff --git a/src/lint/lintProject.ts b/src/lint/lintProject.ts index 89eeafd..2c9c524 100644 --- a/src/lint/lintProject.ts +++ b/src/lint/lintProject.ts @@ -1,4 +1,4 @@ -import { getProjectRoot } from '../utils/getProjectRoot' +import { getProjectRoot } from '../utils' import { lintFolder } from './lintFolder' /** @@ -8,6 +8,7 @@ import { lintFolder } from './lintFolder' export const lintProject = async () => { const projectRoot = (await getProjectRoot()) || process.projectDir || process.currentDir + if (!projectRoot) { throw new Error('SASjs Project Root was not found.') } diff --git a/src/lint/shared.spec.ts b/src/lint/shared.spec.ts new file mode 100644 index 0000000..668610b --- /dev/null +++ b/src/lint/shared.spec.ts @@ -0,0 +1,25 @@ +import { splitText } from './shared' + +describe('splitText', () => { + it('should return an empty array when text is falsy', () => { + const lines = splitText('') + + expect(lines.length).toEqual(0) + }) + + it('should return an array of lines from text', () => { + const lines = splitText(`line 1\nline 2`) + + expect(lines.length).toEqual(2) + expect(lines[0]).toEqual('line 1') + expect(lines[1]).toEqual('line 2') + }) + + it('should work with CRLF line endings', () => { + const lines = splitText(`line 1\r\nline 2`) + + expect(lines.length).toEqual(2) + expect(lines[0]).toEqual('line 1') + expect(lines[1]).toEqual('line 2') + }) +}) diff --git a/src/lint/shared.ts b/src/lint/shared.ts index 50ca081..bbbadc6 100644 --- a/src/lint/shared.ts +++ b/src/lint/shared.ts @@ -1,8 +1,17 @@ import { LintConfig, Diagnostic } from '../types' -import { splitText } from '../utils' + +/** + * Splits the given content into a list of lines, regardless of CRLF or LF line endings. + * @param {string} text - the text content to be split into lines. + * @returns {string[]} an array of lines from the given text + */ +export const splitText = (text: string): string[] => { + if (!text) return [] + return text.replace(/\r\n/g, '\n').split('\n') +} export const processText = (text: string, config: LintConfig) => { - const lines = splitText(text, config) + const lines = splitText(text) const diagnostics: Diagnostic[] = [] diagnostics.push(...processContent(config, text)) lines.forEach((line, index) => { diff --git a/src/rules/file/hasDoxygenHeader.spec.ts b/src/rules/file/hasDoxygenHeader.spec.ts index 9f66a73..2bf1259 100644 --- a/src/rules/file/hasDoxygenHeader.spec.ts +++ b/src/rules/file/hasDoxygenHeader.spec.ts @@ -1,4 +1,3 @@ -import { LintConfig } from '../../types' import { Severity } from '../../types/Severity' import { hasDoxygenHeader } from './hasDoxygenHeader' @@ -69,43 +68,4 @@ describe('hasDoxygenHeader', () => { } ]) }) - - it('should not alter the text if a doxygen header is already present', () => { - const content = `/** - @file - @brief Returns an unused libref - **/ - - %macro mf_getuniquelibref(prefix=mclib,maxtries=1000); - %local x libref; - %let x={SAS002}; - %do x=0 %to &maxtries;` - - expect(hasDoxygenHeader.fix!(content)).toEqual(content) - }) - - it('should should add a doxygen header if not present', () => { - const content = `%macro mf_getuniquelibref(prefix=mclib,maxtries=1000); -%local x libref; -%let x={SAS002}; -%do x=0 %to &maxtries;` - - expect(hasDoxygenHeader.fix!(content)).toEqual( - `/** - @file - @brief -**/` + - '\n' + - content - ) - }) - - it('should use CRLF line endings when configured', () => { - const content = `%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);\n%local x libref;\n%let x={SAS002};\n%do x=0 %to &maxtries;` - const config = new LintConfig({ lineEndings: 'crlf' }) - - expect(hasDoxygenHeader.fix!(content, config)).toEqual( - `/**\r\n @file\r\n @brief \r\n**/` + '\r\n' + content - ) - }) }) diff --git a/src/rules/file/hasDoxygenHeader.ts b/src/rules/file/hasDoxygenHeader.ts index b6635e6..aa7c0bc 100644 --- a/src/rules/file/hasDoxygenHeader.ts +++ b/src/rules/file/hasDoxygenHeader.ts @@ -1,11 +1,7 @@ -import { LintConfig } from '../../types' -import { LineEndings } from '../../types/LineEndings' import { FileLintRule } from '../../types/LintRule' import { LintRuleType } from '../../types/LintRuleType' import { Severity } from '../../types/Severity' -const DoxygenHeader = `/**{lineEnding} @file{lineEnding} @brief {lineEnding}**/` - const name = 'hasDoxygenHeader' const description = 'Enforce the presence of a Doxygen header at the start of each file.' @@ -36,19 +32,6 @@ const test = (value: string) => { } } -const fix = (value: string, config?: LintConfig): string => { - if (test(value).length === 0) { - return value - } - const lineEndingConfig = config?.lineEndings || LineEndings.LF - const lineEnding = lineEndingConfig === LineEndings.LF ? '\n' : '\r\n' - - return `${DoxygenHeader.replace( - /{lineEnding}/g, - lineEnding - )}${lineEnding}${value}` -} - /** * Lint rule that checks for the presence of a Doxygen header in a given file. */ @@ -57,6 +40,5 @@ export const hasDoxygenHeader: FileLintRule = { name, description, message, - test, - fix + test } diff --git a/src/rules/file/hasMacroNameInMend.ts b/src/rules/file/hasMacroNameInMend.ts index 56a4a75..fec97cb 100644 --- a/src/rules/file/hasMacroNameInMend.ts +++ b/src/rules/file/hasMacroNameInMend.ts @@ -2,97 +2,99 @@ 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 { getColumnNumber } from '../../utils/getColumnNumber' -import { LintConfig } from '../../types' -import { LineEndings } from '../../types/LineEndings' -import { parseMacros } from '../../utils/parseMacros' const name = 'hasMacroNameInMend' const description = 'Enforces the presence of the macro name in each %mend statement.' const message = '%mend statement has missing or incorrect macro name' -const test = (value: string, config?: LintConfig) => { - const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n' - const lines: string[] = value ? value.split(lineEnding) : [] - const macros = parseMacros(value, config) +const test = (value: string) => { const diagnostics: Diagnostic[] = [] - macros.forEach((macro) => { - if (macro.startLineNumber === null && macro.endLineNumber !== null) { - diagnostics.push({ - message: `%mend statement is redundant`, - lineNumber: macro.endLineNumber, - startColumnNumber: getColumnNumber( - lines[macro.endLineNumber - 1], - '%mend' - ), - endColumnNumber: - getColumnNumber(lines[macro.endLineNumber - 1], '%mend') + - lines[macro.endLineNumber - 1].trim().length - - 1, - severity: Severity.Warning - }) - } else if (macro.endLineNumber === null && macro.startLineNumber !== null) { - diagnostics.push({ - message: `Missing %mend statement for macro - ${macro.name}`, - lineNumber: macro.startLineNumber, - startColumnNumber: 1, - endColumnNumber: 1, - severity: Severity.Warning - }) - } else if (macro.mismatchedMendMacroName) { - diagnostics.push({ - message: `%mend statement has mismatched macro name, it should be '${ - macro!.name - }'`, - lineNumber: macro.endLineNumber as number, - startColumnNumber: getColumnNumber( - lines[(macro.endLineNumber as number) - 1], - macro.mismatchedMendMacroName - ), - endColumnNumber: - getColumnNumber( - lines[(macro.endLineNumber as number) - 1], - macro.mismatchedMendMacroName - ) + - macro.mismatchedMendMacroName.length - - 1, - severity: Severity.Warning - }) - } else if (!macro.hasMacroNameInMend) { - diagnostics.push({ - message: `%mend statement is missing macro name - ${macro.name}`, - lineNumber: macro.endLineNumber as number, - startColumnNumber: getColumnNumber( - lines[(macro.endLineNumber as number) - 1], - '%mend' - ), - endColumnNumber: - getColumnNumber(lines[(macro.endLineNumber as number) - 1], '%mend') + - 6, - severity: Severity.Warning - }) - } + + const lines: string[] = value ? value.split('\n') : [] + + const declaredMacros: { name: string; lineNumber: number }[] = [] + let isCommentStarted = false + lines.forEach((line, lineIndex) => { + const { statement: trimmedLine, commentStarted } = trimComments( + line, + isCommentStarted + ) + isCommentStarted = commentStarted + const statements: string[] = trimmedLine ? trimmedLine.split(';') : [] + + statements.forEach((statement) => { + const { statement: trimmedStatement, commentStarted } = trimComments( + statement, + isCommentStarted + ) + isCommentStarted = commentStarted + + if (trimmedStatement.startsWith('%macro ')) { + const macroName = trimmedStatement + .slice(7, trimmedStatement.length) + .trim() + .split('(')[0] + if (macroName) + declaredMacros.push({ + name: macroName, + lineNumber: lineIndex + 1 + }) + } else if (trimmedStatement.startsWith('%mend')) { + const declaredMacro = declaredMacros.pop() + const macroName = trimmedStatement + .split(' ') + .filter((s: string) => !!s)[1] + + if (!declaredMacro) { + diagnostics.push({ + message: `%mend statement is redundant`, + lineNumber: lineIndex + 1, + startColumnNumber: getColumnNumber(line, '%mend'), + endColumnNumber: + getColumnNumber(line, '%mend') + trimmedStatement.length, + severity: Severity.Warning + }) + } else if (!macroName) { + diagnostics.push({ + message: `%mend statement is missing macro name - ${ + declaredMacro!.name + }`, + lineNumber: lineIndex + 1, + startColumnNumber: getColumnNumber(line, '%mend'), + endColumnNumber: getColumnNumber(line, '%mend') + 6, + severity: Severity.Warning + }) + } else if (macroName !== declaredMacro!.name) { + diagnostics.push({ + message: `%mend statement has mismatched macro name, it should be '${ + declaredMacro!.name + }'`, + lineNumber: lineIndex + 1, + startColumnNumber: getColumnNumber(line, macroName), + endColumnNumber: + getColumnNumber(line, macroName) + macroName.length - 1, + severity: Severity.Warning + }) + } + } + }) + }) + + declaredMacros.forEach((declaredMacro) => { + diagnostics.push({ + message: `Missing %mend statement for macro - ${declaredMacro.name}`, + lineNumber: declaredMacro.lineNumber, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }) }) return diagnostics } -const fix = (value: string, config?: LintConfig): string => { - const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n' - let formattedText = value - const macros = parseMacros(value, config) - macros - .filter((macro) => !macro.hasMacroNameInMend) - .forEach((macro) => { - formattedText = formattedText.replace( - macro.termination, - `%mend ${macro.name};${lineEnding}` - ) - }) - - return formattedText -} - /** * Lint rule that checks for the presence of macro name in %mend statement. */ @@ -101,6 +103,5 @@ export const hasMacroNameInMend: FileLintRule = { name, description, message, - test, - fix + test } diff --git a/src/rules/file/hasMacroParentheses.spec.ts b/src/rules/file/hasMacroParentheses.spec.ts index f59b6ce..438f054 100644 --- a/src/rules/file/hasMacroParentheses.spec.ts +++ b/src/rules/file/hasMacroParentheses.spec.ts @@ -16,6 +16,7 @@ describe('hasMacroParentheses', () => { %macro somemacro; %put &sysmacroname; %mend somemacro;` + expect(hasMacroParentheses.test(content)).toEqual([ { message: 'Macro definition missing parentheses', @@ -27,7 +28,7 @@ describe('hasMacroParentheses', () => { ]) }) - it('should return an array with a single diagnostic when macro defined without name', () => { + it('should return an array with a single diagnostics when macro defined without name', () => { const content = ` %macro (); %put &sysmacroname; @@ -44,7 +45,7 @@ describe('hasMacroParentheses', () => { ]) }) - it('should return an array with a single diagnostic when macro defined without name and parentheses', () => { + it('should return an array with a single diagnostics when macro defined without name and parentheses', () => { const content = ` %macro ; %put &sysmacroname; @@ -55,7 +56,7 @@ describe('hasMacroParentheses', () => { message: 'Macro definition missing name', lineNumber: 2, startColumnNumber: 3, - endColumnNumber: 10, + endColumnNumber: 9, severity: Severity.Warning } ]) diff --git a/src/rules/file/hasMacroParentheses.ts b/src/rules/file/hasMacroParentheses.ts index f2eef10..9597674 100644 --- a/src/rules/file/hasMacroParentheses.ts +++ b/src/rules/file/hasMacroParentheses.ts @@ -2,51 +2,74 @@ 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 { getColumnNumber } from '../../utils/getColumnNumber' -import { parseMacros } from '../../utils/parseMacros' -import { LintConfig } from '../../types' const name = 'hasMacroParentheses' const description = 'Enforces the presence of parentheses in macro definitions.' const message = 'Macro definition missing parentheses' -const test = (value: string, config?: LintConfig) => { +const test = (value: string) => { const diagnostics: Diagnostic[] = [] - const macros = parseMacros(value, config) - macros.forEach((macro) => { - if (!macro.name) { - diagnostics.push({ - message: 'Macro definition missing name', - lineNumber: macro.startLineNumber!, - startColumnNumber: getColumnNumber(macro.declaration, '%macro'), - endColumnNumber: macro.declaration.length, - severity: Severity.Warning - }) - } else if (!macro.declaration.includes('(')) { - diagnostics.push({ - message, - lineNumber: macro.startLineNumber!, - startColumnNumber: getColumnNumber(macro.declaration, macro.name), - endColumnNumber: - getColumnNumber(macro.declaration, 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.declaration, macro.name), - endColumnNumber: - getColumnNumber(macro.declaration, macro.name) + - macro.name.length - - 1 + - `()`.length, - severity: Severity.Warning - }) - } - }) + const lines: string[] = value ? value.split('\n') : [] + let isCommentStarted = false + lines.forEach((line, lineIndex) => { + const { statement: trimmedLine, commentStarted } = trimComments( + line, + isCommentStarted + ) + isCommentStarted = commentStarted + const statements: string[] = trimmedLine ? trimmedLine.split(';') : [] + + statements.forEach((statement) => { + const { statement: trimmedStatement, commentStarted } = trimComments( + statement, + isCommentStarted + ) + isCommentStarted = 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: lineIndex + 1, + startColumnNumber: getColumnNumber(line, '%macro'), + endColumnNumber: + getColumnNumber(line, '%macro') + trimmedStatement.length, + severity: Severity.Warning + }) + else if (macroNameDefinitionParts.length === 1) + diagnostics.push({ + message, + lineNumber: lineIndex + 1, + startColumnNumber: getColumnNumber(line, macroNameDefinition), + endColumnNumber: + getColumnNumber(line, macroNameDefinition) + + macroNameDefinition.length - + 1, + severity: Severity.Warning + }) + else if (macroName !== macroName.trim()) + diagnostics.push({ + message: 'Macro definition contains space(s)', + lineNumber: lineIndex + 1, + startColumnNumber: getColumnNumber(line, macroNameDefinition), + endColumnNumber: + getColumnNumber(line, macroNameDefinition) + + macroNameDefinition.length - + 1, + severity: Severity.Warning + }) + } + }) + }) return diagnostics } diff --git a/src/rules/file/index.ts b/src/rules/file/index.ts index 40730af..e551bdd 100644 --- a/src/rules/file/index.ts +++ b/src/rules/file/index.ts @@ -1,5 +1,4 @@ export { hasDoxygenHeader } from './hasDoxygenHeader' export { hasMacroNameInMend } from './hasMacroNameInMend' export { hasMacroParentheses } from './hasMacroParentheses' -export { lineEndings } from './lineEndings' export { noNestedMacros } from './noNestedMacros' diff --git a/src/rules/file/lineEndings.spec.ts b/src/rules/file/lineEndings.spec.ts deleted file mode 100644 index d50bf24..0000000 --- a/src/rules/file/lineEndings.spec.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { LintConfig, Severity } from '../../types' -import { LineEndings } from '../../types/LineEndings' -import { lineEndings } from './lineEndings' - -describe('lineEndings', () => { - it('should return an empty array when the text contains the configured line endings', () => { - const text = "%put 'hello';\n%put 'world';\n" - const config = new LintConfig({ lineEndings: LineEndings.LF }) - expect(lineEndings.test(text, config)).toEqual([]) - }) - - it('should return an array with a single diagnostic when a line is terminated with a CRLF ending', () => { - const text = "%put 'hello';\n%put 'world';\r\n" - const config = new LintConfig({ lineEndings: LineEndings.LF }) - expect(lineEndings.test(text, config)).toEqual([ - { - message: 'Incorrect line ending - CRLF instead of LF', - lineNumber: 2, - startColumnNumber: 13, - endColumnNumber: 14, - severity: Severity.Warning - } - ]) - }) - - it('should return an array with a single diagnostic when a line is terminated with an LF ending', () => { - const text = "%put 'hello';\n%put 'world';\r\n" - const config = new LintConfig({ lineEndings: LineEndings.CRLF }) - expect(lineEndings.test(text, config)).toEqual([ - { - message: 'Incorrect line ending - LF instead of CRLF', - lineNumber: 1, - startColumnNumber: 13, - endColumnNumber: 14, - severity: Severity.Warning - } - ]) - }) - - it('should return an array with a diagnostic for each line terminated with an LF ending', () => { - const text = "%put 'hello';\n%put 'test';\r\n%put 'world';\n" - const config = new LintConfig({ lineEndings: LineEndings.CRLF }) - expect(lineEndings.test(text, config)).toContainEqual({ - message: 'Incorrect line ending - LF instead of CRLF', - lineNumber: 1, - startColumnNumber: 13, - endColumnNumber: 14, - severity: Severity.Warning - }) - expect(lineEndings.test(text, config)).toContainEqual({ - message: 'Incorrect line ending - LF instead of CRLF', - lineNumber: 3, - startColumnNumber: 13, - endColumnNumber: 14, - severity: Severity.Warning - }) - }) - - it('should return an array with a diagnostic for each line terminated with a CRLF ending', () => { - const text = "%put 'hello';\r\n%put 'test';\n%put 'world';\r\n" - const config = new LintConfig({ lineEndings: LineEndings.LF }) - expect(lineEndings.test(text, config)).toContainEqual({ - message: 'Incorrect line ending - CRLF instead of LF', - lineNumber: 1, - startColumnNumber: 13, - endColumnNumber: 14, - severity: Severity.Warning - }) - expect(lineEndings.test(text, config)).toContainEqual({ - message: 'Incorrect line ending - CRLF instead of LF', - lineNumber: 3, - startColumnNumber: 13, - endColumnNumber: 14, - severity: Severity.Warning - }) - }) - - it('should return an array with a diagnostic for lines terminated with a CRLF ending', () => { - const text = - "%put 'hello';\r\n%put 'test';\r\n%put 'world';\n%put 'test2';\n%put 'world2';\r\n" - const config = new LintConfig({ lineEndings: LineEndings.LF }) - expect(lineEndings.test(text, config)).toContainEqual({ - message: 'Incorrect line ending - CRLF instead of LF', - lineNumber: 1, - startColumnNumber: 13, - endColumnNumber: 14, - severity: Severity.Warning - }) - expect(lineEndings.test(text, config)).toContainEqual({ - message: 'Incorrect line ending - CRLF instead of LF', - lineNumber: 2, - startColumnNumber: 12, - endColumnNumber: 13, - severity: Severity.Warning - }) - expect(lineEndings.test(text, config)).toContainEqual({ - message: 'Incorrect line ending - CRLF instead of LF', - lineNumber: 5, - startColumnNumber: 14, - endColumnNumber: 15, - severity: Severity.Warning - }) - }) - - it('should transform line endings to LF', () => { - const text = - "%put 'hello';\r\n%put 'test';\r\n%put 'world';\n%put 'test2';\n%put 'world2';\r\n" - const config = new LintConfig({ lineEndings: LineEndings.LF }) - - const formattedText = lineEndings.fix!(text, config) - - expect(formattedText).toEqual( - "%put 'hello';\n%put 'test';\n%put 'world';\n%put 'test2';\n%put 'world2';\n" - ) - }) - - it('should transform line endings to CRLF', () => { - const text = - "%put 'hello';\r\n%put 'test';\r\n%put 'world';\n%put 'test2';\n%put 'world2';\r\n" - const config = new LintConfig({ lineEndings: LineEndings.CRLF }) - - const formattedText = lineEndings.fix!(text, config) - - expect(formattedText).toEqual( - "%put 'hello';\r\n%put 'test';\r\n%put 'world';\r\n%put 'test2';\r\n%put 'world2';\r\n" - ) - }) - - it('should use LF line endings by default', () => { - const text = - "%put 'hello';\r\n%put 'test';\r\n%put 'world';\n%put 'test2';\n%put 'world2';\r\n" - - const formattedText = lineEndings.fix!(text) - - expect(formattedText).toEqual( - "%put 'hello';\n%put 'test';\n%put 'world';\n%put 'test2';\n%put 'world2';\n" - ) - }) -}) diff --git a/src/rules/file/lineEndings.ts b/src/rules/file/lineEndings.ts deleted file mode 100644 index 77cf17a..0000000 --- a/src/rules/file/lineEndings.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Diagnostic, LintConfig } from '../../types' -import { LineEndings } from '../../types/LineEndings' -import { FileLintRule } from '../../types/LintRule' -import { LintRuleType } from '../../types/LintRuleType' -import { Severity } from '../../types/Severity' - -const name = 'lineEndings' -const description = 'Ensures line endings conform to the configured type.' -const message = 'Incorrect line ending - {actual} instead of {expected}' -const test = (value: string, config?: LintConfig) => { - const lineEndingConfig = config?.lineEndings || LineEndings.LF - const expectedLineEnding = - lineEndingConfig === LineEndings.LF ? '{lf}' : '{crlf}' - const incorrectLineEnding = expectedLineEnding === '{lf}' ? '{crlf}' : '{lf}' - - const lines = value - .replace(/\r\n/g, '{crlf}') - .replace(/\n/g, '{lf}') - .split(new RegExp(`(?<=${expectedLineEnding})`)) - const diagnostics: Diagnostic[] = [] - - let indexOffset = 0 - lines.forEach((line, index) => { - if (line.endsWith(incorrectLineEnding)) { - diagnostics.push({ - message: message - .replace('{expected}', expectedLineEnding === '{lf}' ? 'LF' : 'CRLF') - .replace('{actual}', incorrectLineEnding === '{lf}' ? 'LF' : 'CRLF'), - lineNumber: index + 1 + indexOffset, - startColumnNumber: line.indexOf(incorrectLineEnding), - endColumnNumber: line.indexOf(incorrectLineEnding) + 1, - severity: Severity.Warning - }) - } else { - const splitLine = line.split(new RegExp(`(?<=${incorrectLineEnding})`)) - if (splitLine.length > 1) { - indexOffset += splitLine.length - 1 - } - splitLine.forEach((l, i) => { - if (l.endsWith(incorrectLineEnding)) { - diagnostics.push({ - message: message - .replace( - '{expected}', - expectedLineEnding === '{lf}' ? 'LF' : 'CRLF' - ) - .replace( - '{actual}', - incorrectLineEnding === '{lf}' ? 'LF' : 'CRLF' - ), - lineNumber: index + i + 1, - startColumnNumber: l.indexOf(incorrectLineEnding), - endColumnNumber: l.indexOf(incorrectLineEnding) + 1, - severity: Severity.Warning - }) - } - }) - } - }) - return diagnostics -} - -const fix = (value: string, config?: LintConfig): string => { - const lineEndingConfig = config?.lineEndings || LineEndings.LF - - return value - .replace(/\r\n/g, '{crlf}') - .replace(/\n/g, '{lf}') - .replace(/{crlf}/g, lineEndingConfig === LineEndings.LF ? '\n' : '\r\n') - .replace(/{lf}/g, lineEndingConfig === LineEndings.LF ? '\n' : '\r\n') -} - -/** - * Lint rule that checks if line endings in a file match the configured type. - */ -export const lineEndings: FileLintRule = { - type: LintRuleType.File, - name, - description, - message, - test, - fix -} diff --git a/src/rules/file/noNestedMacros.spec.ts b/src/rules/file/noNestedMacros.spec.ts index c2488d0..b9ad5c9 100644 --- a/src/rules/file/noNestedMacros.spec.ts +++ b/src/rules/file/noNestedMacros.spec.ts @@ -29,13 +29,13 @@ describe('noNestedMacros', () => { message: "Macro definition for 'inner' present in macro 'outer'", lineNumber: 4, startColumnNumber: 7, - endColumnNumber: 21, + endColumnNumber: 20, severity: Severity.Warning } ]) }) - it('should return an array with two diagnostics when nested macros are defined at 2 levels', () => { + it('should return an array with a single diagnostic when nested macros are defined at 2 levels', () => { const content = ` %macro outer(); /* any amount of arbitrary code */ @@ -52,20 +52,22 @@ describe('noNestedMacros', () => { %outer()` - expect(noNestedMacros.test(content)).toContainEqual({ - message: "Macro definition for 'inner' present in macro 'outer'", - lineNumber: 4, - startColumnNumber: 7, - endColumnNumber: 21, - severity: Severity.Warning - }) - expect(noNestedMacros.test(content)).toContainEqual({ - message: "Macro definition for 'inner2' present in macro 'inner'", - lineNumber: 7, - startColumnNumber: 17, - endColumnNumber: 32, - severity: Severity.Warning - }) + expect(noNestedMacros.test(content)).toEqual([ + { + message: "Macro definition for 'inner' present in macro 'outer'", + lineNumber: 4, + startColumnNumber: 7, + endColumnNumber: 20, + severity: Severity.Warning + }, + { + message: "Macro definition for 'inner2' present in macro 'inner'", + lineNumber: 7, + startColumnNumber: 17, + endColumnNumber: 31, + severity: Severity.Warning + } + ]) }) it('should return an empty array when the file is undefined', () => { diff --git a/src/rules/file/noNestedMacros.ts b/src/rules/file/noNestedMacros.ts index 338ae15..dca0802 100644 --- a/src/rules/file/noNestedMacros.ts +++ b/src/rules/file/noNestedMacros.ts @@ -2,41 +2,57 @@ 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 { getColumnNumber } from '../../utils/getColumnNumber' -import { parseMacros } from '../../utils/parseMacros' -import { LintConfig } from '../../types' -import { LineEndings } from '../../types/LineEndings' const name = 'noNestedMacros' const description = 'Enfoces the absence of nested macro definitions.' const message = `Macro definition for '{macro}' present in macro '{parent}'` -const test = (value: string, config?: LintConfig) => { - const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n' - const lines: string[] = value ? value.split(lineEnding) : [] +const test = (value: string) => { const diagnostics: Diagnostic[] = [] - const macros = parseMacros(value, config) - macros - .filter((m) => !!m.parentMacro) - .forEach((macro) => { - diagnostics.push({ - message: message - .replace('{macro}', macro.name) - .replace('{parent}', macro.parentMacro), - lineNumber: macro.startLineNumber as number, - startColumnNumber: getColumnNumber( - lines[(macro.startLineNumber as number) - 1], - '%macro' - ), - endColumnNumber: - getColumnNumber( - lines[(macro.startLineNumber as number) - 1], - '%macro' - ) + - lines[(macro.startLineNumber as number) - 1].trim().length - - 1, - severity: Severity.Warning - }) + const declaredMacros: string[] = [] + + const lines: string[] = value ? value.split('\n') : [] + let isCommentStarted = false + lines.forEach((line, lineIndex) => { + const { statement: trimmedLine, commentStarted } = trimComments( + line, + isCommentStarted + ) + isCommentStarted = commentStarted + const statements: string[] = trimmedLine ? trimmedLine.split(';') : [] + + statements.forEach((statement) => { + const { statement: trimmedStatement, commentStarted } = trimComments( + statement, + isCommentStarted + ) + isCommentStarted = commentStarted + + if (trimmedStatement.startsWith('%macro ')) { + const macroName = trimmedStatement + .slice(7, trimmedStatement.length) + .trim() + .split('(')[0] + if (declaredMacros.length) { + const parentMacro = declaredMacros.slice(-1).pop() + diagnostics.push({ + message: message + .replace('{macro}', macroName) + .replace('{parent}', parentMacro!), + lineNumber: lineIndex + 1, + startColumnNumber: getColumnNumber(line, '%macro'), + endColumnNumber: + getColumnNumber(line, '%macro') + trimmedStatement.length - 1, + severity: Severity.Warning + }) + } + declaredMacros.push(macroName) + } else if (trimmedStatement.startsWith('%mend')) { + declaredMacros.pop() + } }) + }) return diagnostics } diff --git a/src/rules/line/noTrailingSpaces.ts b/src/rules/line/noTrailingSpaces.ts index 2200a87..0fe4bc1 100644 --- a/src/rules/line/noTrailingSpaces.ts +++ b/src/rules/line/noTrailingSpaces.ts @@ -17,7 +17,6 @@ const test = (value: string, lineNumber: number) => severity: Severity.Warning } ] -const fix = (value: string) => value.trimEnd() /** * Lint rule that checks for the presence of trailing space(s) in a given line of text. @@ -27,6 +26,5 @@ export const noTrailingSpaces: LineLintRule = { name, description, message, - test, - fix + test } diff --git a/src/types/LineEndings.ts b/src/types/LineEndings.ts deleted file mode 100644 index e40f19b..0000000 --- a/src/types/LineEndings.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum LineEndings { - LF = 'lf', - CRLF = 'crlf' -} diff --git a/src/types/LintConfig.spec.ts b/src/types/LintConfig.spec.ts index 67ac80f..3ea3bb5 100644 --- a/src/types/LintConfig.spec.ts +++ b/src/types/LintConfig.spec.ts @@ -1,4 +1,3 @@ -import { LineEndings } from './LineEndings' import { LintConfig } from './LintConfig' import { LintRuleType } from './LintRuleType' @@ -109,33 +108,6 @@ describe('LintConfig', () => { expect(config.indentationMultiple).toEqual(0) }) - it('should create an instance with the line endings set to LF', () => { - const config = new LintConfig({ lineEndings: 'lf' }) - - expect(config).toBeTruthy() - expect(config.lineEndings).toEqual(LineEndings.LF) - }) - - it('should create an instance with the line endings set to CRLF', () => { - const config = new LintConfig({ lineEndings: 'crlf' }) - - expect(config).toBeTruthy() - expect(config.lineEndings).toEqual(LineEndings.CRLF) - }) - - it('should create an instance with the line endings set to LF by default', () => { - const config = new LintConfig({}) - - expect(config).toBeTruthy() - expect(config.lineEndings).toEqual(LineEndings.LF) - }) - - it('should throw an error with an invalid value for line endings', () => { - expect(() => new LintConfig({ lineEndings: 'test' })).toThrowError( - `Invalid value for lineEndings: can be ${LineEndings.LF} or ${LineEndings.CRLF}` - ) - }) - it('should create an instance with all flags set', () => { const config = new LintConfig({ noTrailingSpaces: true, diff --git a/src/types/LintConfig.ts b/src/types/LintConfig.ts index d7f2f88..3dea4b1 100644 --- a/src/types/LintConfig.ts +++ b/src/types/LintConfig.ts @@ -2,8 +2,7 @@ import { hasDoxygenHeader, hasMacroNameInMend, noNestedMacros, - hasMacroParentheses, - lineEndings + hasMacroParentheses } from '../rules/file' import { indentationMultiple, @@ -13,7 +12,6 @@ import { noTrailingSpaces } from '../rules/line' import { lowerCaseFileNames, noSpacesInFileNames } from '../rules/path' -import { LineEndings } from './LineEndings' import { FileLintRule, LineLintRule, PathLintRule } from './LintRule' /** @@ -29,7 +27,6 @@ export class LintConfig { readonly pathLintRules: PathLintRule[] = [] readonly maxLineLength: number = 80 readonly indentationMultiple: number = 2 - readonly lineEndings: LineEndings = LineEndings.LF constructor(json?: any) { if (json?.noTrailingSpaces) { @@ -49,19 +46,6 @@ export class LintConfig { this.lineLintRules.push(maxLineLength) } - if (json?.lineEndings) { - if ( - json.lineEndings !== LineEndings.LF && - json.lineEndings !== LineEndings.CRLF - ) { - throw new Error( - `Invalid value for lineEndings: can be ${LineEndings.LF} or ${LineEndings.CRLF}` - ) - } - this.lineEndings = json.lineEndings - this.fileLintRules.push(lineEndings) - } - if (!isNaN(json?.indentationMultiple)) { this.indentationMultiple = json.indentationMultiple as number this.lineLintRules.push(indentationMultiple) diff --git a/src/types/LintRule.ts b/src/types/LintRule.ts index f32a58c..d3fbf29 100644 --- a/src/types/LintRule.ts +++ b/src/types/LintRule.ts @@ -19,7 +19,6 @@ export interface LintRule { export interface LineLintRule extends LintRule { type: LintRuleType.Line test: (value: string, lineNumber: number, config?: LintConfig) => Diagnostic[] - fix?: (value: string, config?: LintConfig) => string } /** @@ -27,8 +26,7 @@ export interface LineLintRule extends LintRule { */ export interface FileLintRule extends LintRule { type: LintRuleType.File - test: (value: string, config?: LintConfig) => Diagnostic[] - fix?: (value: string, config?: LintConfig) => string + test: (value: string) => Diagnostic[] } /** diff --git a/src/utils/index.ts b/src/utils/index.ts index b0a151b..f48b820 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,3 @@ export * from './getLintConfig' export * from './getProjectRoot' export * from './listSasFiles' -export * from './splitText' diff --git a/src/utils/parseMacros.spec.ts b/src/utils/parseMacros.spec.ts deleted file mode 100644 index 1367523..0000000 --- a/src/utils/parseMacros.spec.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { LintConfig } from '../types' -import { parseMacros } from './parseMacros' - -describe('parseMacros', () => { - 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', - declaration: '%macro test;', - termination: '%mend', - startLineNumber: 1, - endLineNumber: 3, - parentMacro: '', - hasMacroNameInMend: false, - hasParentheses: false, - mismatchedMendMacroName: '' - }) - }) - - it('should return an array with multiple macros', () => { - const text = `%macro foo; - %put 'foo'; -%mend; -%macro bar(); - %put 'bar'; -%mend bar;` - - const macros = parseMacros(text, new LintConfig()) - - expect(macros.length).toEqual(2) - expect(macros).toContainEqual({ - name: 'foo', - declaration: '%macro foo;', - termination: '%mend;', - startLineNumber: 1, - endLineNumber: 3, - parentMacro: '', - hasMacroNameInMend: false, - hasParentheses: false, - mismatchedMendMacroName: '' - }) - expect(macros).toContainEqual({ - name: 'bar', - declaration: '%macro bar();', - termination: '%mend bar;', - startLineNumber: 4, - endLineNumber: 6, - parentMacro: '', - hasMacroNameInMend: true, - hasParentheses: true, - mismatchedMendMacroName: '' - }) - }) - - it('should detect nested macro definitions', () => { - const text = `%macro test() - %put 'hello'; - %macro test2 - %put 'world; - %mend -%mend test` - - const macros = parseMacros(text, new LintConfig()) - - expect(macros.length).toEqual(2) - expect(macros).toContainEqual({ - name: 'test', - declaration: '%macro test()', - termination: '%mend test', - startLineNumber: 1, - endLineNumber: 6, - parentMacro: '', - hasMacroNameInMend: true, - hasParentheses: true, - mismatchedMendMacroName: '' - }) - expect(macros).toContainEqual({ - name: 'test2', - declaration: ' %macro test2', - termination: ' %mend', - startLineNumber: 3, - endLineNumber: 5, - parentMacro: 'test', - hasMacroNameInMend: false, - hasParentheses: false, - mismatchedMendMacroName: '' - }) - }) -}) diff --git a/src/utils/parseMacros.ts b/src/utils/parseMacros.ts deleted file mode 100644 index 65ecd24..0000000 --- a/src/utils/parseMacros.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { LintConfig } from '../types/LintConfig' -import { LineEndings } from '../types/LineEndings' -import { trimComments } from './trimComments' - -interface Macro { - name: string - startLineNumber: number | null - endLineNumber: number | null - declaration: string - termination: string - parentMacro: string - hasMacroNameInMend: boolean - hasParentheses: 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) : [] - const macros: Macro[] = [] - - let isCommentStarted = false - let macroStack: Macro[] = [] - lines.forEach((line, index) => { - const { statement: trimmedLine, commentStarted } = trimComments( - line, - isCommentStarted - ) - isCommentStarted = commentStarted - const statements: string[] = trimmedLine ? trimmedLine.split(';') : [] - - statements.forEach((statement) => { - const { statement: trimmedStatement, commentStarted } = trimComments( - statement, - isCommentStarted - ) - isCommentStarted = commentStarted - - if (trimmedStatement.startsWith('%macro')) { - const startLineNumber = index + 1 - const name = trimmedStatement - .slice(7, trimmedStatement.length) - .trim() - .split('(')[0] - macroStack.push({ - name, - startLineNumber, - endLineNumber: null, - parentMacro: macroStack.length - ? macroStack[macroStack.length - 1].name - : '', - hasParentheses: trimmedStatement.endsWith('()'), - hasMacroNameInMend: false, - mismatchedMendMacroName: '', - declaration: line, - termination: '' - }) - } else if (trimmedStatement.startsWith('%mend')) { - if (macroStack.length) { - const macro = macroStack.pop() as Macro - const mendMacroName = - trimmedStatement.split(' ').filter((s: string) => !!s)[1] || '' - macro.endLineNumber = index + 1 - macro.hasMacroNameInMend = trimmedStatement.includes(macro.name) - macro.mismatchedMendMacroName = macro.hasMacroNameInMend - ? '' - : mendMacroName - macro.termination = line - macros.push(macro) - } else { - macros.push({ - name: '', - startLineNumber: null, - endLineNumber: index + 1, - parentMacro: '', - hasParentheses: false, - hasMacroNameInMend: false, - mismatchedMendMacroName: '', - declaration: '', - termination: line - }) - } - } - }) - }) - - macros.push(...macroStack) - - return macros -} diff --git a/src/utils/splitText.spec.ts b/src/utils/splitText.spec.ts deleted file mode 100644 index b6a2531..0000000 --- a/src/utils/splitText.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { LintConfig } from '../types' -import { splitText } from './splitText' - -describe('splitText', () => { - const config = new LintConfig({ - noTrailingSpaces: true, - noEncodedPasswords: true, - hasDoxygenHeader: true, - noSpacesInFileNames: true, - maxLineLength: 80, - lowerCaseFileNames: true, - noTabIndentation: true, - indentationMultiple: 2, - hasMacroNameInMend: true, - noNestedMacros: true, - hasMacroParentheses: true, - lineEndings: 'lf' - }) - - it('should return an empty array when text is falsy', () => { - const lines = splitText('', config) - - expect(lines.length).toEqual(0) - }) - - it('should return an array of lines from text', () => { - const lines = splitText(`line 1\nline 2`, config) - - expect(lines.length).toEqual(2) - expect(lines[0]).toEqual('line 1') - expect(lines[1]).toEqual('line 2') - }) - - it('should work with CRLF line endings', () => { - const lines = splitText(`line 1\r\nline 2`, config) - - expect(lines.length).toEqual(2) - expect(lines[0]).toEqual('line 1') - expect(lines[1]).toEqual('line 2') - }) -}) diff --git a/src/utils/splitText.ts b/src/utils/splitText.ts deleted file mode 100644 index 498230e..0000000 --- a/src/utils/splitText.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { LintConfig } from '../types/LintConfig' -import { LineEndings } from '../types/LineEndings' - -/** - * Splits the given content into a list of lines, regardless of CRLF or LF line endings. - * @param {string} text - the text content to be split into lines. - * @returns {string[]} an array of lines from the given text - */ -export const splitText = (text: string, config: LintConfig): string[] => { - if (!text) return [] - const expectedLineEndings = - config.lineEndings === LineEndings.LF ? '\n' : '\r\n' - const incorrectLineEndings = expectedLineEndings === '\n' ? '\r\n' : '\n' - return text - .replace(new RegExp(incorrectLineEndings, 'g'), expectedLineEndings) - .split(expectedLineEndings) -}