1
0
mirror of https://github.com/sasjs/lint.git synced 2026-01-17 17:20:05 +00:00

Merge pull request #24 from sasjs/assorted-fixes

This commit is contained in:
Krishna Acondy
2021-04-07 15:52:14 +01:00
committed by GitHub
16 changed files with 414 additions and 175 deletions

View File

@@ -16,5 +16,6 @@ What code changes have been made to achieve the intent.
- [ ] Any new functionality has been unit tested. - [ ] Any new functionality has been unit tested.
- [ ] All unit tests are passing (`npm test`). - [ ] All unit tests are passing (`npm test`).
- [ ] All CI checks are green. - [ ] All CI checks are green.
- [ ] sasjslint-schema.json is updated with any new / changed functionality
- [ ] JSDoc comments have been added or updated. - [ ] JSDoc comments have been added or updated.
- [ ] Reviewer is assigned. - [ ] Reviewer is assigned.

View File

@@ -5,14 +5,17 @@
"title": "SASjs Lint Config File", "title": "SASjs Lint Config File",
"description": "The SASjs Lint Config file provides the settings for customising SAS code style in your project.", "description": "The SASjs Lint Config file provides the settings for customising SAS code style in your project.",
"default": { "default": {
"noTrailingSpaces": true,
"noEncodedPasswords": true, "noEncodedPasswords": true,
"hasDoxygenHeader": true, "hasDoxygenHeader": true,
"noSpacesInFileNames": true, "hasMacroNameInMend": false,
"hasMacroParentheses": true,
"indentationMultiple": 2,
"lowerCaseFileNames": true, "lowerCaseFileNames": true,
"maxLineLength": 80, "maxLineLength": 80,
"noNestedMacros": true,
"noSpacesInFileNames": true,
"noTabIndentation": true, "noTabIndentation": true,
"indentationMultiple": 2 "noTrailingSpaces": true
}, },
"examples": [ "examples": [
{ {
@@ -23,18 +26,13 @@
"lowerCaseFileNames": true, "lowerCaseFileNames": true,
"maxLineLength": 80, "maxLineLength": 80,
"noTabIndentation": true, "noTabIndentation": true,
"indentationMultiple": 4 "indentationMultiple": 4,
"hasMacroNameInMend": true,
"noNestedMacros": true,
"hasMacroParentheses": true
} }
], ],
"properties": { "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": { "noEncodedPasswords": {
"$id": "#/properties/noEncodedPasswords", "$id": "#/properties/noEncodedPasswords",
"type": "boolean", "type": "boolean",
@@ -51,14 +49,30 @@
"default": true, "default": true,
"examples": [true, false] "examples": [true, false]
}, },
"noSpacesInFileNames": { "hasMacroNameInMend": {
"$id": "#/properties/noSpacesInFileNames", "$id": "#/properties/hasMacroNameInMend",
"type": "boolean", "type": "boolean",
"title": "noSpacesInFileNames", "title": "hasMacroNameInMend",
"description": "Enforces no spaces in file names. Shows a warning when they are present.", "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, "default": true,
"examples": [true, false] "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": { "lowerCaseFileNames": {
"$id": "#/properties/lowerCaseFileNames", "$id": "#/properties/lowerCaseFileNames",
"type": "boolean", "type": "boolean",
@@ -75,6 +89,22 @@
"default": 80, "default": 80,
"examples": [60, 80, 120] "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": { "noTabIndentation": {
"$id": "#/properties/noTabIndentation", "$id": "#/properties/noTabIndentation",
"type": "boolean", "type": "boolean",
@@ -83,13 +113,13 @@
"default": true, "default": true,
"examples": [true, false] "examples": [true, false]
}, },
"indentationMultiple": { "noTrailingSpaces": {
"$id": "#/properties/indentationMultiple", "$id": "#/properties/noTrailingSpaces",
"type": "number", "type": "boolean",
"title": "indentationMultiple", "title": "noTrailingSpaces",
"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.", "description": "Enforces no trailing spaces in lines of SAS code. Shows a warning when they are present.",
"default": 2, "default": true,
"examples": [2, 3, 4] "examples": [true, false]
} }
} }
} }

View File

@@ -28,7 +28,7 @@ describe('hasMacroNameInMend', () => {
expect(hasMacroNameInMend.test(content)).toEqual([ expect(hasMacroNameInMend.test(content)).toEqual([
{ {
message: '%mend missing macro name', message: '%mend statement is missing macro name - somemacro',
lineNumber: 4, lineNumber: 4,
startColumnNumber: 3, startColumnNumber: 3,
endColumnNumber: 9, 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', () => { it('should return an array with a single diagnostic when %mend has incorrect macro name', () => {
const content = ` const content = `
%macro somemacro; %macro somemacro;
@@ -45,7 +83,7 @@ describe('hasMacroNameInMend', () => {
expect(hasMacroNameInMend.test(content)).toEqual([ 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, lineNumber: 4,
startColumnNumber: 9, startColumnNumber: 9,
endColumnNumber: 24, 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', () => { it('should return an empty array when the file is undefined', () => {
const content = undefined const content = undefined
@@ -88,7 +144,7 @@ describe('hasMacroNameInMend', () => {
expect(hasMacroNameInMend.test(content)).toEqual([ expect(hasMacroNameInMend.test(content)).toEqual([
{ {
message: '%mend missing macro name', message: '%mend statement is missing macro name - inner',
lineNumber: 6, lineNumber: 6,
startColumnNumber: 5, startColumnNumber: 5,
endColumnNumber: 11, endColumnNumber: 11,
@@ -110,7 +166,7 @@ describe('hasMacroNameInMend', () => {
expect(hasMacroNameInMend.test(content)).toEqual([ expect(hasMacroNameInMend.test(content)).toEqual([
{ {
message: '%mend missing macro name', message: '%mend statement is missing macro name - outer',
lineNumber: 9, lineNumber: 9,
startColumnNumber: 3, startColumnNumber: 3,
endColumnNumber: 9, endColumnNumber: 9,
@@ -132,14 +188,14 @@ describe('hasMacroNameInMend', () => {
expect(hasMacroNameInMend.test(content)).toEqual([ expect(hasMacroNameInMend.test(content)).toEqual([
{ {
message: '%mend missing macro name', message: '%mend statement is missing macro name - inner',
lineNumber: 6, lineNumber: 6,
startColumnNumber: 5, startColumnNumber: 5,
endColumnNumber: 11, endColumnNumber: 11,
severity: Severity.Warning severity: Severity.Warning
}, },
{ {
message: '%mend missing macro name', message: '%mend statement is missing macro name - outer',
lineNumber: 9, lineNumber: 9,
startColumnNumber: 3, startColumnNumber: 3,
endColumnNumber: 9, endColumnNumber: 9,
@@ -197,7 +253,7 @@ describe('hasMacroNameInMend', () => {
expect(hasMacroNameInMend.test(content)).toEqual([ expect(hasMacroNameInMend.test(content)).toEqual([
{ {
message: '%mend missing macro name', message: '%mend statement is missing macro name - examplemacro',
lineNumber: 29, lineNumber: 29,
startColumnNumber: 5, startColumnNumber: 5,
endColumnNumber: 11, endColumnNumber: 11,
@@ -216,7 +272,7 @@ describe('hasMacroNameInMend', () => {
expect(hasMacroNameInMend.test(content)).toEqual([ 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, lineNumber: 6,
startColumnNumber: 14, startColumnNumber: 14,
endColumnNumber: 29, endColumnNumber: 29,
@@ -233,7 +289,7 @@ describe('hasMacroNameInMend', () => {
expect(hasMacroNameInMend.test(content)).toEqual([ expect(hasMacroNameInMend.test(content)).toEqual([
{ {
message: '%mend missing macro name', message: '%mend statement is missing macro name - somemacro',
lineNumber: 4, lineNumber: 4,
startColumnNumber: 5, startColumnNumber: 5,
endColumnNumber: 11, endColumnNumber: 11,

View File

@@ -3,67 +3,95 @@ import { FileLintRule } from '../types/LintRule'
import { LintRuleType } from '../types/LintRuleType' import { LintRuleType } from '../types/LintRuleType'
import { Severity } from '../types/Severity' import { Severity } from '../types/Severity'
import { trimComments } from '../utils/trimComments' import { trimComments } from '../utils/trimComments'
import { getLineNumber } from '../utils/getLineNumber' import { getColumnNumber } from '../utils/getColumnNumber'
import { getColNumber } from '../utils/getColNumber'
const name = 'hasMacroNameInMend' const name = 'hasMacroNameInMend'
const description = 'The %mend statement should contain the macro name' const description =
const message = '$mend statement missing or incorrect' '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 test = (value: string) => {
const diagnostics: Diagnostic[] = [] const diagnostics: Diagnostic[] = []
const statements: string[] = value ? value.split(';') : [] const lines: string[] = value ? value.split('\n') : []
const stack: string[] = [] const declaredMacros: { name: string; lineNumber: number }[] = []
let trimmedStatement = '', let isCommentStarted = false
commentStarted = false lines.forEach((line, lineIndex) => {
statements.forEach((statement, index) => { const { statement: trimmedLine, commentStarted } = trimComments(
;({ statement: trimmedStatement, commentStarted } = trimComments( line,
statement, isCommentStarted
commentStarted )
)) isCommentStarted = commentStarted
const statements: string[] = trimmedLine ? trimmedLine.split(';') : []
if (trimmedStatement.startsWith('%macro ')) { statements.forEach((statement) => {
const macroName = trimmedStatement const { statement: trimmedStatement, commentStarted } = trimComments(
.slice(7, trimmedStatement.length) statement,
.trim() isCommentStarted
.split('(')[0] )
stack.push(macroName) isCommentStarted = commentStarted
} else if (trimmedStatement.startsWith('%mend')) {
const macroStarted = stack.pop()
const macroName = trimmedStatement
.split(' ')
.filter((s: string) => !!s)[1]
if (!macroName) { if (trimmedStatement.startsWith('%macro ')) {
diagnostics.push({ const macroName = trimmedStatement
message: '%mend missing macro name', .slice(7, trimmedStatement.length)
lineNumber: getLineNumber(statements, index + 1), .trim()
startColumnNumber: getColNumber(statement, '%mend'), .split('(')[0]
endColumnNumber: getColNumber(statement, '%mend') + 6, if (macroName)
severity: Severity.Warning declaredMacros.push({
}) name: macroName,
} else if (macroName !== macroStarted) { lineNumber: lineIndex + 1
diagnostics.push({ })
message: 'mismatch macro name in %mend statement', } else if (trimmedStatement.startsWith('%mend')) {
lineNumber: getLineNumber(statements, index + 1), const declaredMacro = declaredMacros.pop()
startColumnNumber: getColNumber(statement, macroName), const macroName = trimmedStatement
endColumnNumber: .split(' ')
getColNumber(statement, macroName) + macroName.length - 1, .filter((s: string) => !!s)[1]
severity: Severity.Warning
}) 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({ diagnostics.push({
message: 'missing %mend statement for macro(s)', message: `Missing %mend statement for macro - ${declaredMacro.name}`,
lineNumber: statements.length + 1, lineNumber: declaredMacro.lineNumber,
startColumnNumber: 1, startColumnNumber: 1,
endColumnNumber: 1, endColumnNumber: 1,
severity: Severity.Warning severity: Severity.Warning
}) })
} })
return diagnostics return diagnostics
} }

View File

@@ -56,7 +56,7 @@ describe('hasMacroParentheses', () => {
message: 'Macro definition missing name', message: 'Macro definition missing name',
lineNumber: 2, lineNumber: 2,
startColumnNumber: 3, startColumnNumber: 3,
endColumnNumber: 10, endColumnNumber: 9,
severity: Severity.Warning 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
}
])
})
}) })

View File

@@ -3,70 +3,78 @@ import { FileLintRule } from '../types/LintRule'
import { LintRuleType } from '../types/LintRuleType' import { LintRuleType } from '../types/LintRuleType'
import { Severity } from '../types/Severity' import { Severity } from '../types/Severity'
import { trimComments } from '../utils/trimComments' import { trimComments } from '../utils/trimComments'
import { getLineNumber } from '../utils/getLineNumber' import { getColumnNumber } from '../utils/getColumnNumber'
import { getColNumber } from '../utils/getColNumber'
const name = 'hasMacroParentheses' 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 message = 'Macro definition missing parentheses'
const test = (value: string) => { const test = (value: string) => {
const diagnostics: Diagnostic[] = [] 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 = '', statements.forEach((statement) => {
commentStarted = false const { statement: trimmedStatement, commentStarted } = trimComments(
statements.forEach((statement, index) => { statement,
;({ statement: trimmedStatement, commentStarted } = trimComments( isCommentStarted
statement, )
commentStarted isCommentStarted = commentStarted
))
if (trimmedStatement.startsWith('%macro')) { if (trimmedStatement.startsWith('%macro')) {
const macroNameDefinition = trimmedStatement const macroNameDefinition = trimmedStatement
.slice(7, trimmedStatement.length) .slice(7, trimmedStatement.length)
.trim() .trim()
const macroNameDefinitionParts = macroNameDefinition.split('(') const macroNameDefinitionParts = macroNameDefinition.split('(')
const macroName = macroNameDefinitionParts[0] const macroName = macroNameDefinitionParts[0]
if (!macroName) if (!macroName)
diagnostics.push({ diagnostics.push({
message: 'Macro definition missing name', message: 'Macro definition missing name',
lineNumber: getLineNumber(statements, index + 1), lineNumber: lineIndex + 1,
startColumnNumber: getColNumber(statement, '%macro'), startColumnNumber: getColumnNumber(line, '%macro'),
endColumnNumber: statement.length, endColumnNumber:
severity: Severity.Warning getColumnNumber(line, '%macro') + trimmedStatement.length,
}) severity: Severity.Warning
else if (macroNameDefinitionParts.length === 1) })
diagnostics.push({ else if (macroNameDefinitionParts.length === 1)
message, diagnostics.push({
lineNumber: getLineNumber(statements, index + 1), message,
startColumnNumber: getColNumber(statement, macroNameDefinition), lineNumber: lineIndex + 1,
endColumnNumber: startColumnNumber: getColumnNumber(line, macroNameDefinition),
getColNumber(statement, macroNameDefinition) + endColumnNumber:
macroNameDefinition.length - getColumnNumber(line, macroNameDefinition) +
1, macroNameDefinition.length -
severity: Severity.Warning 1,
}) severity: Severity.Warning
else if (macroName !== macroName.trim()) })
diagnostics.push({ else if (macroName !== macroName.trim())
message: 'Macro definition cannot have space', diagnostics.push({
lineNumber: getLineNumber(statements, index + 1), message: 'Macro definition contains space(s)',
startColumnNumber: getColNumber(statement, macroNameDefinition), lineNumber: lineIndex + 1,
endColumnNumber: startColumnNumber: getColumnNumber(line, macroNameDefinition),
getColNumber(statement, macroNameDefinition) + endColumnNumber:
macroNameDefinition.length - getColumnNumber(line, macroNameDefinition) +
1, macroNameDefinition.length -
severity: Severity.Warning 1,
}) severity: Severity.Warning
} })
}
})
}) })
return diagnostics 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 = { export const hasMacroParentheses: FileLintRule = {
type: LintRuleType.File, type: LintRuleType.File,

View File

@@ -11,7 +11,7 @@ describe('noNestedMacros', () => {
expect(noNestedMacros.test(content)).toEqual([]) 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 = ` const content = `
%macro outer(); %macro outer();
/* any amount of arbitrary code */ /* any amount of arbitrary code */
@@ -26,7 +26,7 @@ describe('noNestedMacros', () => {
expect(noNestedMacros.test(content)).toEqual([ expect(noNestedMacros.test(content)).toEqual([
{ {
message: "Macro definition present inside another macro 'outer'", message: "Macro definition for 'inner' present in macro 'outer'",
lineNumber: 4, lineNumber: 4,
startColumnNumber: 7, startColumnNumber: 7,
endColumnNumber: 20, 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 = ` const content = `
%macro outer(); %macro outer();
/* any amount of arbitrary code */ /* any amount of arbitrary code */
@@ -54,14 +54,14 @@ describe('noNestedMacros', () => {
expect(noNestedMacros.test(content)).toEqual([ expect(noNestedMacros.test(content)).toEqual([
{ {
message: "Macro definition present inside another macro 'outer'", message: "Macro definition for 'inner' present in macro 'outer'",
lineNumber: 4, lineNumber: 4,
startColumnNumber: 7, startColumnNumber: 7,
endColumnNumber: 20, endColumnNumber: 20,
severity: Severity.Warning severity: Severity.Warning
}, },
{ {
message: "Macro definition present inside another macro 'inner'", message: "Macro definition for 'inner2' present in macro 'inner'",
lineNumber: 7, lineNumber: 7,
startColumnNumber: 17, startColumnNumber: 17,
endColumnNumber: 31, endColumnNumber: 31,

View File

@@ -3,52 +3,61 @@ import { FileLintRule } from '../types/LintRule'
import { LintRuleType } from '../types/LintRuleType' import { LintRuleType } from '../types/LintRuleType'
import { Severity } from '../types/Severity' import { Severity } from '../types/Severity'
import { trimComments } from '../utils/trimComments' import { trimComments } from '../utils/trimComments'
import { getLineNumber } from '../utils/getLineNumber' import { getColumnNumber } from '../utils/getColumnNumber'
import { getColNumber } from '../utils/getColNumber'
const name = 'noNestedMacros' const name = 'noNestedMacros'
const description = 'Defining nested macro is not good practice' const description = 'Enfoces the absence of nested macro definitions.'
const message = 'Macro definition present inside another macro' const message = `Macro definition for '{macro}' present in macro '{parent}'`
const test = (value: string) => { const test = (value: string) => {
const diagnostics: Diagnostic[] = [] 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[] = [] statements.forEach((statement) => {
let trimmedStatement = '', const { statement: trimmedStatement, commentStarted } = trimComments(
commentStarted = false statement,
statements.forEach((statement, index) => { isCommentStarted
;({ statement: trimmedStatement, commentStarted } = trimComments( )
statement, isCommentStarted = commentStarted
commentStarted
))
if (trimmedStatement.startsWith('%macro ')) { if (trimmedStatement.startsWith('%macro ')) {
const macroName = trimmedStatement const macroName = trimmedStatement
.slice(7, trimmedStatement.length) .slice(7, trimmedStatement.length)
.trim() .trim()
.split('(')[0] .split('(')[0]
if (stack.length) { if (declaredMacros.length) {
const parentMacro = stack.slice(-1).pop() const parentMacro = declaredMacros.slice(-1).pop()
diagnostics.push({ diagnostics.push({
message: `${message} '${parentMacro}'`, message: message
lineNumber: getLineNumber(statements, index + 1), .replace('{macro}', macroName)
startColumnNumber: getColNumber(statement, '%macro'), .replace('{parent}', parentMacro!),
endColumnNumber: lineNumber: lineIndex + 1,
getColNumber(statement, '%macro') + trimmedStatement.length - 1, startColumnNumber: getColumnNumber(line, '%macro'),
severity: Severity.Warning 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 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 = { export const noNestedMacros: FileLintRule = {
type: LintRuleType.File, type: LintRuleType.File,

View File

@@ -1,3 +1,6 @@
/**
* Executes an async callback for each item in the given array.
*/
export async function asyncForEach( export async function asyncForEach(
array: any[], array: any[],
callback: (item: any, index: number, originalArray: any[]) => any callback: (item: any, index: number, originalArray: any[]) => any

View File

@@ -1,3 +0,0 @@
export const getColNumber = (statement: string, text: string): number => {
return (statement.split('\n').pop() as string).indexOf(text) + 1
}

View File

@@ -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'"
)
})
})

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,5 +1,9 @@
import { listFilesInFolder } from '@sasjs/utils/file' import { listFilesInFolder } from '@sasjs/utils/file'
/**
* Fetches a list of .sas files in the given path.
* @returns {Promise<string[]>} resolves with an array of file names.
*/
export const listSasFiles = async (folderPath: string): Promise<string[]> => { export const listSasFiles = async (folderPath: string): Promise<string[]> => {
const files = await listFilesInFolder(folderPath) const files = await listFilesInFolder(folderPath)
return files.filter((f) => f.endsWith('.sas')) return files.filter((f) => f.endsWith('.sas'))

View File

@@ -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 })
})
})

View File

@@ -2,7 +2,7 @@ export const trimComments = (
statement: string, statement: string,
commentStarted: boolean = false commentStarted: boolean = false
): { statement: string; commentStarted: boolean } => { ): { statement: string; commentStarted: boolean } => {
let trimmed = statement.trim() let trimmed = (statement || '').trim()
if (commentStarted || trimmed.startsWith('/*')) { if (commentStarted || trimmed.startsWith('/*')) {
const parts = trimmed.split('*/') const parts = trimmed.split('*/')