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:
@@ -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.
|
||||||
|
|||||||
@@ -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]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export const getColNumber = (statement: string, text: string): number => {
|
|
||||||
return (statement.split('\n').pop() as string).indexOf(text) + 1
|
|
||||||
}
|
|
||||||
13
src/utils/getColumnNumber.spec.ts
Normal file
13
src/utils/getColumnNumber.spec.ts
Normal 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'"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
7
src/utils/getColumnNumber.ts
Normal file
7
src/utils/getColumnNumber.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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'))
|
||||||
|
|||||||
74
src/utils/trimComments.spec.ts
Normal file
74
src/utils/trimComments.spec.ts
Normal 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 })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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('*/')
|
||||||
|
|||||||
Reference in New Issue
Block a user