diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index e6bc3c9..6800f22 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -16,5 +16,6 @@ What code changes have been made to achieve the intent. - [ ] Any new functionality has been unit tested. - [ ] All unit tests are passing (`npm test`). - [ ] All CI checks are green. +- [ ] sasjslint-schema.json is updated with any new / changed functionality - [ ] JSDoc comments have been added or updated. - [ ] Reviewer is assigned. diff --git a/sasjslint-schema.json b/sasjslint-schema.json index 9af64c8..e01f628 100644 --- a/sasjslint-schema.json +++ b/sasjslint-schema.json @@ -5,14 +5,17 @@ "title": "SASjs Lint Config File", "description": "The SASjs Lint Config file provides the settings for customising SAS code style in your project.", "default": { - "noTrailingSpaces": true, "noEncodedPasswords": true, "hasDoxygenHeader": true, - "noSpacesInFileNames": true, + "hasMacroNameInMend": false, + "hasMacroParentheses": true, + "indentationMultiple": 2, "lowerCaseFileNames": true, "maxLineLength": 80, + "noNestedMacros": true, + "noSpacesInFileNames": true, "noTabIndentation": true, - "indentationMultiple": 2 + "noTrailingSpaces": true }, "examples": [ { @@ -23,18 +26,13 @@ "lowerCaseFileNames": true, "maxLineLength": 80, "noTabIndentation": true, - "indentationMultiple": 4 + "indentationMultiple": 4, + "hasMacroNameInMend": true, + "noNestedMacros": true, + "hasMacroParentheses": true } ], "properties": { - "noTrailingSpaces": { - "$id": "#/properties/noTrailingSpaces", - "type": "boolean", - "title": "noTrailingSpaces", - "description": "Enforces no trailing spaces in lines of SAS code. Shows a warning when they are present.", - "default": true, - "examples": [true, false] - }, "noEncodedPasswords": { "$id": "#/properties/noEncodedPasswords", "type": "boolean", @@ -51,14 +49,30 @@ "default": true, "examples": [true, false] }, - "noSpacesInFileNames": { - "$id": "#/properties/noSpacesInFileNames", + "hasMacroNameInMend": { + "$id": "#/properties/hasMacroNameInMend", "type": "boolean", - "title": "noSpacesInFileNames", - "description": "Enforces no spaces in file names. Shows a warning when they are present.", + "title": "hasMacroNameInMend", + "description": "Enforces the presence of macro names in %mend statements. Shows a warning for %mend statements with missing or mismatched macro names.", + "default": false, + "examples": [true, false] + }, + "hasMacroParentheses": { + "$id": "#/properties/hasMacroParentheses", + "type": "boolean", + "title": "hasMacroParentheses", + "description": "Enforces the presence of parentheses in macro definitions. Shows a warning for each macro defined without parentheses, or with spaces between the macro name and the opening parenthesis.", "default": true, "examples": [true, false] }, + "indentationMultiple": { + "$id": "#/properties/indentationMultiple", + "type": "number", + "title": "indentationMultiple", + "description": "Enforces a configurable multiple for the number of spaces for indentation. Shows a warning for lines that are not indented by a multiple of this number.", + "default": 2, + "examples": [2, 3, 4] + }, "lowerCaseFileNames": { "$id": "#/properties/lowerCaseFileNames", "type": "boolean", @@ -75,6 +89,22 @@ "default": 80, "examples": [60, 80, 120] }, + "noNestedMacros": { + "$id": "#/properties/noNestedMacros", + "type": "boolean", + "title": "noNestedMacros", + "description": "Enforces the absence of nested macro definitions. Shows a warning for each nested macro definition.", + "default": true, + "examples": [true, false] + }, + "noSpacesInFileNames": { + "$id": "#/properties/noSpacesInFileNames", + "type": "boolean", + "title": "noSpacesInFileNames", + "description": "Enforces no spaces in file names. Shows a warning when they are present.", + "default": true, + "examples": [true, false] + }, "noTabIndentation": { "$id": "#/properties/noTabIndentation", "type": "boolean", @@ -83,13 +113,13 @@ "default": true, "examples": [true, false] }, - "indentationMultiple": { - "$id": "#/properties/indentationMultiple", - "type": "number", - "title": "indentationMultiple", - "description": "Enforces a configurable multiple for the number of spaces for indentation. Shows a warning for lines that are not indented by a multiple of this number.", - "default": 2, - "examples": [2, 3, 4] + "noTrailingSpaces": { + "$id": "#/properties/noTrailingSpaces", + "type": "boolean", + "title": "noTrailingSpaces", + "description": "Enforces no trailing spaces in lines of SAS code. Shows a warning when they are present.", + "default": true, + "examples": [true, false] } } } diff --git a/src/rules/hasMacroNameInMend.spec.ts b/src/rules/hasMacroNameInMend.spec.ts index 5876305..5cb10c8 100644 --- a/src/rules/hasMacroNameInMend.spec.ts +++ b/src/rules/hasMacroNameInMend.spec.ts @@ -28,7 +28,7 @@ describe('hasMacroNameInMend', () => { expect(hasMacroNameInMend.test(content)).toEqual([ { - message: '%mend missing macro name', + message: '%mend statement is missing macro name - somemacro', lineNumber: 4, startColumnNumber: 3, endColumnNumber: 9, @@ -37,6 +37,44 @@ describe('hasMacroNameInMend', () => { ]) }) + it('should return an array with a single diagnostic when a macro is missing an %mend statement', () => { + const content = `%macro somemacro; + %put &sysmacroname;` + + expect(hasMacroNameInMend.test(content)).toEqual([ + { + message: 'Missing %mend statement for macro - somemacro', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + } + ]) + }) + + it('should return an array with a diagnostic for each macro missing an %mend statement', () => { + const content = `%macro somemacro; + %put &sysmacroname; + %macro othermacro` + + expect(hasMacroNameInMend.test(content)).toEqual([ + { + message: 'Missing %mend statement for macro - somemacro', + lineNumber: 1, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + }, + { + message: 'Missing %mend statement for macro - othermacro', + lineNumber: 3, + startColumnNumber: 1, + endColumnNumber: 1, + severity: Severity.Warning + } + ]) + }) + it('should return an array with a single diagnostic when %mend has incorrect macro name', () => { const content = ` %macro somemacro; @@ -45,7 +83,7 @@ describe('hasMacroNameInMend', () => { expect(hasMacroNameInMend.test(content)).toEqual([ { - message: 'mismatch macro name in %mend statement', + message: `%mend statement has mismatched macro name, it should be 'somemacro'`, lineNumber: 4, startColumnNumber: 9, endColumnNumber: 24, @@ -54,6 +92,24 @@ describe('hasMacroNameInMend', () => { ]) }) + it('should return an array with a single diagnostic when extra %mend statement is present', () => { + const content = ` + %macro somemacro; + %put &sysmacroname; + %mend somemacro; + %mend something;` + + expect(hasMacroNameInMend.test(content)).toEqual([ + { + message: '%mend statement is redundant', + lineNumber: 5, + startColumnNumber: 3, + endColumnNumber: 18, + severity: Severity.Warning + } + ]) + }) + it('should return an empty array when the file is undefined', () => { const content = undefined @@ -88,7 +144,7 @@ describe('hasMacroNameInMend', () => { expect(hasMacroNameInMend.test(content)).toEqual([ { - message: '%mend missing macro name', + message: '%mend statement is missing macro name - inner', lineNumber: 6, startColumnNumber: 5, endColumnNumber: 11, @@ -110,7 +166,7 @@ describe('hasMacroNameInMend', () => { expect(hasMacroNameInMend.test(content)).toEqual([ { - message: '%mend missing macro name', + message: '%mend statement is missing macro name - outer', lineNumber: 9, startColumnNumber: 3, endColumnNumber: 9, @@ -132,14 +188,14 @@ describe('hasMacroNameInMend', () => { expect(hasMacroNameInMend.test(content)).toEqual([ { - message: '%mend missing macro name', + message: '%mend statement is missing macro name - inner', lineNumber: 6, startColumnNumber: 5, endColumnNumber: 11, severity: Severity.Warning }, { - message: '%mend missing macro name', + message: '%mend statement is missing macro name - outer', lineNumber: 9, startColumnNumber: 3, endColumnNumber: 9, @@ -197,7 +253,7 @@ describe('hasMacroNameInMend', () => { expect(hasMacroNameInMend.test(content)).toEqual([ { - message: '%mend missing macro name', + message: '%mend statement is missing macro name - examplemacro', lineNumber: 29, startColumnNumber: 5, endColumnNumber: 11, @@ -216,7 +272,7 @@ describe('hasMacroNameInMend', () => { expect(hasMacroNameInMend.test(content)).toEqual([ { - message: 'mismatch macro name in %mend statement', + message: `%mend statement has mismatched macro name, it should be 'somemacro'`, lineNumber: 6, startColumnNumber: 14, endColumnNumber: 29, @@ -233,7 +289,7 @@ describe('hasMacroNameInMend', () => { expect(hasMacroNameInMend.test(content)).toEqual([ { - message: '%mend missing macro name', + message: '%mend statement is missing macro name - somemacro', lineNumber: 4, startColumnNumber: 5, endColumnNumber: 11, diff --git a/src/rules/hasMacroNameInMend.ts b/src/rules/hasMacroNameInMend.ts index 2081f77..db8deda 100644 --- a/src/rules/hasMacroNameInMend.ts +++ b/src/rules/hasMacroNameInMend.ts @@ -3,67 +3,95 @@ import { FileLintRule } from '../types/LintRule' import { LintRuleType } from '../types/LintRuleType' import { Severity } from '../types/Severity' import { trimComments } from '../utils/trimComments' -import { getLineNumber } from '../utils/getLineNumber' -import { getColNumber } from '../utils/getColNumber' +import { getColumnNumber } from '../utils/getColumnNumber' const name = 'hasMacroNameInMend' -const description = 'The %mend statement should contain the macro name' -const message = '$mend statement missing or incorrect' +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) => { const diagnostics: Diagnostic[] = [] - const statements: string[] = value ? value.split(';') : [] + const lines: string[] = value ? value.split('\n') : [] - const stack: string[] = [] - let trimmedStatement = '', - commentStarted = false - statements.forEach((statement, index) => { - ;({ statement: trimmedStatement, commentStarted } = trimComments( - statement, - commentStarted - )) + 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(';') : [] - if (trimmedStatement.startsWith('%macro ')) { - const macroName = trimmedStatement - .slice(7, trimmedStatement.length) - .trim() - .split('(')[0] - stack.push(macroName) - } else if (trimmedStatement.startsWith('%mend')) { - const macroStarted = stack.pop() - const macroName = trimmedStatement - .split(' ') - .filter((s: string) => !!s)[1] + statements.forEach((statement) => { + const { statement: trimmedStatement, commentStarted } = trimComments( + statement, + isCommentStarted + ) + isCommentStarted = commentStarted - if (!macroName) { - diagnostics.push({ - message: '%mend missing macro name', - lineNumber: getLineNumber(statements, index + 1), - startColumnNumber: getColNumber(statement, '%mend'), - endColumnNumber: getColNumber(statement, '%mend') + 6, - severity: Severity.Warning - }) - } else if (macroName !== macroStarted) { - diagnostics.push({ - message: 'mismatch macro name in %mend statement', - lineNumber: getLineNumber(statements, index + 1), - startColumnNumber: getColNumber(statement, macroName), - endColumnNumber: - getColNumber(statement, macroName) + macroName.length - 1, - severity: Severity.Warning - }) + 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 + }) + } } - } + }) }) - if (stack.length) { + + declaredMacros.forEach((declaredMacro) => { diagnostics.push({ - message: 'missing %mend statement for macro(s)', - lineNumber: statements.length + 1, + message: `Missing %mend statement for macro - ${declaredMacro.name}`, + lineNumber: declaredMacro.lineNumber, startColumnNumber: 1, endColumnNumber: 1, severity: Severity.Warning }) - } + }) + return diagnostics } diff --git a/src/rules/hasMacroParentheses.spec.ts b/src/rules/hasMacroParentheses.spec.ts index bb77ae7..1b75567 100644 --- a/src/rules/hasMacroParentheses.spec.ts +++ b/src/rules/hasMacroParentheses.spec.ts @@ -56,7 +56,7 @@ describe('hasMacroParentheses', () => { message: 'Macro definition missing name', lineNumber: 2, startColumnNumber: 3, - endColumnNumber: 10, + endColumnNumber: 9, severity: Severity.Warning } ]) @@ -125,4 +125,18 @@ 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/hasMacroParentheses.ts b/src/rules/hasMacroParentheses.ts index fb894fa..593867a 100644 --- a/src/rules/hasMacroParentheses.ts +++ b/src/rules/hasMacroParentheses.ts @@ -3,70 +3,78 @@ import { FileLintRule } from '../types/LintRule' import { LintRuleType } from '../types/LintRuleType' import { Severity } from '../types/Severity' import { trimComments } from '../utils/trimComments' -import { getLineNumber } from '../utils/getLineNumber' -import { getColNumber } from '../utils/getColNumber' +import { getColumnNumber } from '../utils/getColumnNumber' const name = 'hasMacroParentheses' -const description = 'Macros are always defined with parentheses' +const description = 'Enforces the presence of parentheses in macro definitions.' const message = 'Macro definition missing parentheses' const test = (value: string) => { const diagnostics: Diagnostic[] = [] - const statements: string[] = value ? value.split(';') : [] + 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(';') : [] - let trimmedStatement = '', - commentStarted = false - statements.forEach((statement, index) => { - ;({ statement: trimmedStatement, commentStarted } = trimComments( - statement, - commentStarted - )) + statements.forEach((statement) => { + const { statement: trimmedStatement, commentStarted } = trimComments( + statement, + isCommentStarted + ) + isCommentStarted = commentStarted - if (trimmedStatement.startsWith('%macro')) { - const macroNameDefinition = trimmedStatement - .slice(7, trimmedStatement.length) - .trim() + if (trimmedStatement.startsWith('%macro')) { + const macroNameDefinition = trimmedStatement + .slice(7, trimmedStatement.length) + .trim() - const macroNameDefinitionParts = macroNameDefinition.split('(') - const macroName = macroNameDefinitionParts[0] + const macroNameDefinitionParts = macroNameDefinition.split('(') + const macroName = macroNameDefinitionParts[0] - if (!macroName) - diagnostics.push({ - message: 'Macro definition missing name', - lineNumber: getLineNumber(statements, index + 1), - startColumnNumber: getColNumber(statement, '%macro'), - endColumnNumber: statement.length, - severity: Severity.Warning - }) - else if (macroNameDefinitionParts.length === 1) - diagnostics.push({ - message, - lineNumber: getLineNumber(statements, index + 1), - startColumnNumber: getColNumber(statement, macroNameDefinition), - endColumnNumber: - getColNumber(statement, macroNameDefinition) + - macroNameDefinition.length - - 1, - severity: Severity.Warning - }) - else if (macroName !== macroName.trim()) - diagnostics.push({ - message: 'Macro definition cannot have space', - lineNumber: getLineNumber(statements, index + 1), - startColumnNumber: getColNumber(statement, macroNameDefinition), - endColumnNumber: - getColNumber(statement, macroNameDefinition) + - macroNameDefinition.length - - 1, - severity: Severity.Warning - }) - } + 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 } /** - * Lint rule that checks for the presence of macro name in %mend statement. + * Lint rule that enforces the presence of parentheses in macro definitions.. */ export const hasMacroParentheses: FileLintRule = { type: LintRuleType.File, diff --git a/src/rules/noNestedMacros.spec.ts b/src/rules/noNestedMacros.spec.ts index a965190..d77aa5b 100644 --- a/src/rules/noNestedMacros.spec.ts +++ b/src/rules/noNestedMacros.spec.ts @@ -11,7 +11,7 @@ describe('noNestedMacros', () => { expect(noNestedMacros.test(content)).toEqual([]) }) - it('should return an array with a single diagnostics when nested macro defined', () => { + it('should return an array with a single diagnostic when a macro contains a nested macro definition', () => { const content = ` %macro outer(); /* any amount of arbitrary code */ @@ -26,7 +26,7 @@ describe('noNestedMacros', () => { expect(noNestedMacros.test(content)).toEqual([ { - message: "Macro definition present inside another macro 'outer'", + message: "Macro definition for 'inner' present in macro 'outer'", lineNumber: 4, startColumnNumber: 7, endColumnNumber: 20, @@ -35,7 +35,7 @@ describe('noNestedMacros', () => { ]) }) - it('should return an array with a single diagnostics when nested macro defined 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 */ @@ -54,14 +54,14 @@ describe('noNestedMacros', () => { expect(noNestedMacros.test(content)).toEqual([ { - message: "Macro definition present inside another macro 'outer'", + message: "Macro definition for 'inner' present in macro 'outer'", lineNumber: 4, startColumnNumber: 7, endColumnNumber: 20, severity: Severity.Warning }, { - message: "Macro definition present inside another macro 'inner'", + message: "Macro definition for 'inner2' present in macro 'inner'", lineNumber: 7, startColumnNumber: 17, endColumnNumber: 31, diff --git a/src/rules/noNestedMacros.ts b/src/rules/noNestedMacros.ts index 2fb5fc9..69568a7 100644 --- a/src/rules/noNestedMacros.ts +++ b/src/rules/noNestedMacros.ts @@ -3,52 +3,61 @@ import { FileLintRule } from '../types/LintRule' import { LintRuleType } from '../types/LintRuleType' import { Severity } from '../types/Severity' import { trimComments } from '../utils/trimComments' -import { getLineNumber } from '../utils/getLineNumber' -import { getColNumber } from '../utils/getColNumber' +import { getColumnNumber } from '../utils/getColumnNumber' const name = 'noNestedMacros' -const description = 'Defining nested macro is not good practice' -const message = 'Macro definition present inside another macro' +const description = 'Enfoces the absence of nested macro definitions.' +const message = `Macro definition for '{macro}' present in macro '{parent}'` const test = (value: string) => { const diagnostics: Diagnostic[] = [] + const declaredMacros: string[] = [] - const statements: string[] = value ? value.split(';') : [] + 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(';') : [] - const stack: string[] = [] - let trimmedStatement = '', - commentStarted = false - statements.forEach((statement, index) => { - ;({ statement: trimmedStatement, commentStarted } = trimComments( - statement, - commentStarted - )) + 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 (stack.length) { - const parentMacro = stack.slice(-1).pop() - diagnostics.push({ - message: `${message} '${parentMacro}'`, - lineNumber: getLineNumber(statements, index + 1), - startColumnNumber: getColNumber(statement, '%macro'), - endColumnNumber: - getColNumber(statement, '%macro') + trimmedStatement.length - 1, - severity: Severity.Warning - }) + 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() } - stack.push(macroName) - } else if (trimmedStatement.startsWith('%mend')) { - stack.pop() - } + }) }) return diagnostics } /** - * Lint rule that checks for the presence of macro name in %mend statement. + * Lint rule that checks for the absence of nested macro definitions. */ export const noNestedMacros: FileLintRule = { type: LintRuleType.File, diff --git a/src/utils/asyncForEach.ts b/src/utils/asyncForEach.ts index 744052b..9ec4f5e 100644 --- a/src/utils/asyncForEach.ts +++ b/src/utils/asyncForEach.ts @@ -1,3 +1,6 @@ +/** + * Executes an async callback for each item in the given array. + */ export async function asyncForEach( array: any[], callback: (item: any, index: number, originalArray: any[]) => any diff --git a/src/utils/getColNumber.ts b/src/utils/getColNumber.ts deleted file mode 100644 index 4333bd6..0000000 --- a/src/utils/getColNumber.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const getColNumber = (statement: string, text: string): number => { - return (statement.split('\n').pop() as string).indexOf(text) + 1 -} diff --git a/src/utils/getColumnNumber.spec.ts b/src/utils/getColumnNumber.spec.ts new file mode 100644 index 0000000..85a0c1a --- /dev/null +++ b/src/utils/getColumnNumber.spec.ts @@ -0,0 +1,13 @@ +import { getColumnNumber } from './getColumnNumber' + +describe('getColumnNumber', () => { + it('should return the column number of the specified string within a line of text', () => { + expect(getColumnNumber('foo bar', 'bar')).toEqual(5) + }) + + it('should throw an error when the specified string is not found within the text', () => { + expect(() => getColumnNumber('foo bar', 'baz')).toThrowError( + "String 'baz' was not found in line 'foo bar'" + ) + }) +}) diff --git a/src/utils/getColumnNumber.ts b/src/utils/getColumnNumber.ts new file mode 100644 index 0000000..1361c5a --- /dev/null +++ b/src/utils/getColumnNumber.ts @@ -0,0 +1,7 @@ +export const getColumnNumber = (line: string, text: string): number => { + const index = (line.split('\n').pop() as string).indexOf(text) + if (index < 0) { + throw new Error(`String '${text}' was not found in line '${line}'`) + } + return (line.split('\n').pop() as string).indexOf(text) + 1 +} diff --git a/src/utils/getLineNumber.ts b/src/utils/getLineNumber.ts deleted file mode 100644 index ebfe055..0000000 --- a/src/utils/getLineNumber.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const getLineNumber = (statements: string[], index: number): number => { - const combinedCode = statements.slice(0, index).join(';') - const lines = (combinedCode.match(/\n/g) || []).length + 1 - return lines -} diff --git a/src/utils/listSasFiles.ts b/src/utils/listSasFiles.ts index 47899d6..bc2530a 100644 --- a/src/utils/listSasFiles.ts +++ b/src/utils/listSasFiles.ts @@ -1,5 +1,9 @@ import { listFilesInFolder } from '@sasjs/utils/file' +/** + * Fetches a list of .sas files in the given path. + * @returns {Promise} resolves with an array of file names. + */ export const listSasFiles = async (folderPath: string): Promise => { const files = await listFilesInFolder(folderPath) return files.filter((f) => f.endsWith('.sas')) diff --git a/src/utils/trimComments.spec.ts b/src/utils/trimComments.spec.ts new file mode 100644 index 0000000..4ede9c3 --- /dev/null +++ b/src/utils/trimComments.spec.ts @@ -0,0 +1,74 @@ +import { trimComments } from './trimComments' + +describe('trimComments', () => { + it('should return statment', () => { + expect( + trimComments(` + /* some comment */ some code; + `) + ).toEqual({ statement: 'some code;', commentStarted: false }) + }) + + it('should return statment, having multi-line comment', () => { + expect( + trimComments(` + /* some + comment */ + some code; + `) + ).toEqual({ statement: 'some code;', commentStarted: false }) + }) + + it('should return statment, having multi-line comment and some code present in comment', () => { + expect( + trimComments(` + /* some + some code; + comment */ + some other code; + `) + ).toEqual({ statement: 'some other code;', commentStarted: false }) + }) + + it('should return empty statment, having only comment', () => { + expect( + trimComments(` + /* some + some code; + comment */ + `) + ).toEqual({ statement: '', commentStarted: false }) + }) + + it('should return empty statment, having continuity in comment', () => { + expect( + trimComments(` + /* some + some code; + `) + ).toEqual({ statement: '', commentStarted: true }) + }) + + it('should return statment, having already started comment and ends', () => { + expect( + trimComments( + ` + comment */ + some code; + `, + true + ) + ).toEqual({ statement: 'some code;', commentStarted: false }) + }) + + it('should return empty statment, having already started comment and continuity in comment', () => { + expect( + trimComments( + ` + some code; + `, + true + ) + ).toEqual({ statement: '', commentStarted: true }) + }) +}) diff --git a/src/utils/trimComments.ts b/src/utils/trimComments.ts index ad11691..509dd34 100644 --- a/src/utils/trimComments.ts +++ b/src/utils/trimComments.ts @@ -2,7 +2,7 @@ export const trimComments = ( statement: string, commentStarted: boolean = false ): { statement: string; commentStarted: boolean } => { - let trimmed = statement.trim() + let trimmed = (statement || '').trim() if (commentStarted || trimmed.startsWith('/*')) { const parts = trimmed.split('*/')