1
0
mirror of https://github.com/sasjs/lint.git synced 2025-12-11 01:44:36 +00:00

Compare commits

..

21 Commits

Author SHA1 Message Date
Krishna Acondy
984915fe47 Merge pull request #29 from sasjs/line-ending-formatting
feat(*): add line endings rule, add automatic formatting for fixable violations
2021-04-21 15:24:08 +01:00
Krishna Acondy
2687a8fa46 chore(*): separate tests for test and fix functions 2021-04-21 15:22:02 +01:00
Krishna Acondy
3da3e1e134 fix(macros): check for exact match with macro name 2021-04-21 15:17:16 +01:00
Krishna Acondy
abc2f75dc0 chore(*): rename macro properties 2021-04-21 15:10:28 +01:00
Saad Jutt
060b838f21 test(*): removed extra lineEndings 2021-04-21 16:51:13 +05:00
Saad Jutt
cd90b0850a fix(hasMacroParentheses): added additional test also 2021-04-21 16:44:20 +05:00
Saad Jutt
db2dbb1c69 feat(format): rules for hasMacroNameInMend 2021-04-21 16:25:36 +05:00
Saad Jutt
59f7e71919 tests(hasMacroNameInMend): Added more 2021-04-21 03:31:14 +05:00
Saad Jutt
6fd941aa2d tests(hasMacroNameInMend): Added more 2021-04-21 03:27:24 +05:00
Krishna Acondy
93124bec5b chore(*): revert change to example file 2021-04-19 22:15:11 +01:00
Krishna Acondy
bcb50b9968 feat(format): add the ability to format files, folders and projects 2021-04-19 22:13:53 +01:00
Krishna Acondy
d28d32d441 fix(*): add SAS Macros section to Doxygen header 2021-04-19 21:07:24 +01:00
Krishna Acondy
519a0164b5 feat(*): add line endings rule, add automatic formatting for fixable violations 2021-04-19 21:00:38 +01:00
Krishna Acondy
99813f04c0 chore(*): fix tests 2021-04-19 20:55:59 +01:00
Krishna Acondy
eb5a1bbbcb Revert "feat(*): add line endings rule, add automatic formatting for fixable violations"
This reverts commit 33a57c3163.
2021-04-19 20:46:38 +01:00
Krishna Acondy
0c22ade942 Merge branch 'main' of https://github.com/sasjs/lint into main 2021-04-19 20:06:51 +01:00
Krishna Acondy
c2209cbe0e Merge pull request #27 from sasjs/issue-26
fix(hasMacroNameInMend): default sets to true
2021-04-16 13:24:40 +01:00
Saad Jutt
fe974050f7 chore: replaced with Severity.Warning 2021-04-16 16:20:02 +05:00
Saad Jutt
1402802f0a tests: added expectedDiagnostics array 2021-04-16 16:16:22 +05:00
Saad Jutt
36b3a7f319 chore(tests): changed numeric literals -> consts 2021-04-15 20:45:27 +05:00
Saad Jutt
c56887d6e6 fix(hasMacroNameInMend): default sets to true 2021-04-15 16:33:31 +05:00
27 changed files with 899 additions and 297 deletions

View File

@@ -9,6 +9,5 @@
"indentationMultiple": 2,
"hasMacroNameInMend": true,
"noNestedMacros": true,
"hasMacroParentheses": true,
"lineEndings": "lf"
"hasMacroParentheses": true
}

View File

@@ -15,7 +15,8 @@
"noNestedMacros": true,
"noSpacesInFileNames": true,
"noTabIndentation": true,
"noTrailingSpaces": true
"noTrailingSpaces": true,
"lineEndings": "lf"
},
"examples": [
{
@@ -29,7 +30,8 @@
"indentationMultiple": 4,
"hasMacroNameInMend": true,
"noNestedMacros": true,
"hasMacroParentheses": true
"hasMacroParentheses": true,
"lineEndings": "crlf"
}
],
"properties": {
@@ -120,6 +122,14 @@
"description": "Enforces no trailing spaces in lines of SAS code. Shows a warning when they are present.",
"default": true,
"examples": [true, false]
},
"lineEndings": {
"$id": "#/properties/lineEndings",
"type": "string",
"title": "lineEndings",
"description": "Enforces the configured terminating character for each line. Shows a warning when incorrect line endings are present.",
"default": "lf",
"examples": ["lf", "crlf"]
}
}
}

View File

@@ -0,0 +1,42 @@
import { formatFile } from './formatFile'
import path from 'path'
import { createFile, deleteFile, readFile } from '@sasjs/utils/file'
import { LintConfig } from '../types'
describe('formatFile', () => {
it('should fix linting issues in a given file', async () => {
const content = `%macro somemacro(); \n%put 'hello';\n%mend;`
const expectedContent = `/**\n @file\n @brief <Your brief here>\n <h4> SAS Macros </h4>\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;`
await createFile(path.join(__dirname, 'format-file-test.sas'), content)
await formatFile(path.join(__dirname, 'format-file-test.sas'))
const result = await readFile(path.join(__dirname, 'format-file-test.sas'))
expect(result).toEqual(expectedContent)
await deleteFile(path.join(__dirname, 'format-file-test.sas'))
})
it('should use the provided config if available', async () => {
const content = `%macro somemacro(); \n%put 'hello';\n%mend;`
const expectedContent = `/**\r\n @file\r\n @brief <Your brief here>\r\n <h4> SAS Macros </h4>\r\n**/\r\n%macro somemacro();\r\n%put 'hello';\r\n%mend somemacro;`
await createFile(path.join(__dirname, 'format-file-config.sas'), content)
await formatFile(
path.join(__dirname, 'format-file-config.sas'),
new LintConfig({
lineEndings: 'crlf',
hasMacroNameInMend: true,
hasDoxygenHeader: true,
noTrailingSpaces: true
})
)
const result = await readFile(
path.join(__dirname, 'format-file-config.sas')
)
expect(result).toEqual(expectedContent)
await deleteFile(path.join(__dirname, 'format-file-config.sas'))
})
})

22
src/format/formatFile.ts Normal file
View File

@@ -0,0 +1,22 @@
import { createFile, readFile } from '@sasjs/utils/file'
import { LintConfig } from '../types/LintConfig'
import { getLintConfig } from '../utils/getLintConfig'
import { processText } from './shared'
/**
* Applies automatic formatting to the file at the given path.
* @param {string} filePath - the path to the file to be formatted.
* @param {LintConfig} configuration - an optional configuration. When not passed in, this is read from the .sasjslint file.
* @returns {Promise<void>} Resolves successfully when the file has been formatted.
*/
export const formatFile = async (
filePath: string,
configuration?: LintConfig
) => {
const config = configuration || (await getLintConfig())
const text = await readFile(filePath)
const formattedText = processText(text, config)
await createFile(filePath, formattedText)
}

View File

@@ -0,0 +1,59 @@
import { formatFolder } from './formatFolder'
import path from 'path'
import {
createFile,
createFolder,
deleteFolder,
readFile
} from '@sasjs/utils/file'
describe('formatFolder', () => {
it('should fix linting issues in a given folder', async () => {
const content = `%macro somemacro(); \n%put 'hello';\n%mend;`
const expectedContent = `/**\n @file\n @brief <Your brief here>\n <h4> SAS Macros </h4>\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;`
await createFolder(path.join(__dirname, 'format-folder-test'))
await createFile(
path.join(__dirname, 'format-folder-test', 'format-folder-test.sas'),
content
)
await formatFolder(path.join(__dirname, 'format-folder-test'))
const result = await readFile(
path.join(__dirname, 'format-folder-test', 'format-folder-test.sas')
)
expect(result).toEqual(expectedContent)
await deleteFolder(path.join(__dirname, 'format-folder-test'))
})
it('should fix linting issues in subfolders of a given folder', async () => {
const content = `%macro somemacro(); \n%put 'hello';\n%mend;`
const expectedContent = `/**\n @file\n @brief <Your brief here>\n <h4> SAS Macros </h4>\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;`
await createFolder(path.join(__dirname, 'format-folder-test'))
await createFolder(path.join(__dirname, 'subfolder'))
await createFile(
path.join(
__dirname,
'format-folder-test',
'subfolder',
'format-folder-test.sas'
),
content
)
await formatFolder(path.join(__dirname, 'format-folder-test'))
const result = await readFile(
path.join(
__dirname,
'format-folder-test',
'subfolder',
'format-folder-test.sas'
)
)
expect(result).toEqual(expectedContent)
await deleteFolder(path.join(__dirname, 'format-folder-test'))
})
})

View File

@@ -0,0 +1,42 @@
import { listSubFoldersInFolder } from '@sasjs/utils/file'
import path from 'path'
import { LintConfig } from '../types/LintConfig'
import { asyncForEach } from '../utils/asyncForEach'
import { getLintConfig } from '../utils/getLintConfig'
import { listSasFiles } from '../utils/listSasFiles'
import { formatFile } from './formatFile'
const excludeFolders = [
'.git',
'.github',
'.vscode',
'node_modules',
'sasjsbuild',
'sasjsresults'
]
/**
* Automatically formats all SAS files in the folder at the given path.
* @param {string} folderPath - the path to the folder to be formatted.
* @param {LintConfig} configuration - an optional configuration. When not passed in, this is read from the .sasjslint file.
* @returns {Promise<void>} Resolves successfully when all SAS files in the given folder have been formatted.
*/
export const formatFolder = async (
folderPath: string,
configuration?: LintConfig
) => {
const config = configuration || (await getLintConfig())
const fileNames = await listSasFiles(folderPath)
await asyncForEach(fileNames, async (fileName) => {
const filePath = path.join(folderPath, fileName)
await formatFile(filePath)
})
const subFolders = (await listSubFoldersInFolder(folderPath)).filter(
(f: string) => !excludeFolders.includes(f)
)
await asyncForEach(subFolders, async (subFolder) => {
await formatFolder(path.join(folderPath, subFolder), config)
})
}

View File

@@ -0,0 +1,51 @@
import { formatProject } from './formatProject'
import path from 'path'
import {
createFile,
createFolder,
deleteFolder,
readFile
} from '@sasjs/utils/file'
import { DefaultLintConfiguration } from '../utils'
import * as getProjectRootModule from '../utils/getProjectRoot'
jest.mock('../utils/getProjectRoot')
describe('formatProject', () => {
it('should format files in the current project', async () => {
const content = `%macro somemacro(); \n%put 'hello';\n%mend;`
const expectedContent = `/**\n @file\n @brief <Your brief here>\n <h4> SAS Macros </h4>\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;`
await createFolder(path.join(__dirname, 'format-project-test'))
await createFile(
path.join(__dirname, 'format-project-test', 'format-project-test.sas'),
content
)
await createFile(
path.join(__dirname, 'format-project-test', '.sasjslint'),
JSON.stringify(DefaultLintConfiguration)
)
jest
.spyOn(getProjectRootModule, 'getProjectRoot')
.mockImplementation(() =>
Promise.resolve(path.join(__dirname, 'format-project-test'))
)
await formatProject()
const result = await readFile(
path.join(__dirname, 'format-project-test', 'format-project-test.sas')
)
expect(result).toEqual(expectedContent)
await deleteFolder(path.join(__dirname, 'format-project-test'))
})
it('should throw an error when a project root is not found', async () => {
jest
.spyOn(getProjectRootModule, 'getProjectRoot')
.mockImplementationOnce(() => Promise.resolve(''))
await expect(formatProject()).rejects.toThrowError(
'SASjs Project Root was not found.'
)
})
})

View File

@@ -0,0 +1,15 @@
import { getProjectRoot } from '../utils/getProjectRoot'
import { formatFolder } from './formatFolder'
/**
* Automatically formats all SAS files in the current project.
* @returns {Promise<void>} Resolves successfully when all SAS files in the current project have been formatted.
*/
export const formatProject = async () => {
const projectRoot =
(await getProjectRoot()) || process.projectDir || process.currentDir
if (!projectRoot) {
throw new Error('SASjs Project Root was not found.')
}
return await formatFolder(projectRoot)
}

View File

@@ -18,8 +18,9 @@ describe('formatText', () => {
const expectedOutput = `/**
@file
@brief <Your brief here>
<h4> SAS Macros </h4>
**/\n%macro test
%put 'hello';\n%mend;`
%put 'hello';\n%mend test;`
const output = await formatText(text)
@@ -39,7 +40,7 @@ describe('formatText', () => {
)
const text = `%macro test\n %put 'hello';\r\n%mend; `
const expectedOutput = `/**\r\n @file\r\n @brief <Your brief here>\r\n**/\r\n%macro test\r\n %put 'hello';\r\n%mend;`
const expectedOutput = `/**\r\n @file\r\n @brief <Your brief here>\r\n <h4> SAS Macros </h4>\r\n**/\r\n%macro test\r\n %put 'hello';\r\n%mend test;`
const output = await formatText(text)

4
src/format/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export * from './formatText'
export * from './formatFile'
export * from './formatFolder'
export * from './formatProject'

View File

@@ -1,3 +1,4 @@
export * from './format'
export * from './lint'
export * from './types'
export * from './utils'

View File

@@ -2,68 +2,87 @@ import { lintFile } from './lintFile'
import { Severity } from '../types/Severity'
import path from 'path'
const expectedDiagnostics = [
{
message: 'Line contains trailing spaces',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 2,
severity: Severity.Warning
},
{
message: 'Line contains trailing spaces',
lineNumber: 2,
startColumnNumber: 1,
endColumnNumber: 2,
severity: Severity.Warning
},
{
message: 'File name contains spaces',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
},
{
message: 'File name contains uppercase characters',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
},
{
message: 'File missing Doxygen header',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
},
{
message: 'Line contains encoded password',
lineNumber: 5,
startColumnNumber: 10,
endColumnNumber: 18,
severity: Severity.Error
},
{
message: 'Line is indented with a tab',
lineNumber: 7,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
},
{
message: 'Line has incorrect indentation - 3 spaces',
lineNumber: 6,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
},
{
message: '%mend statement is missing macro name - mf_getuniquelibref',
lineNumber: 17,
startColumnNumber: 3,
endColumnNumber: 9,
severity: Severity.Warning
}
]
describe('lintFile', () => {
it('should identify lint issues in a given file', async () => {
const results = await lintFile(
path.join(__dirname, '..', 'Example File.sas')
)
expect(results.length).toEqual(8)
expect(results).toContainEqual({
message: 'Line contains trailing spaces',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 2,
severity: Severity.Warning
})
expect(results).toContainEqual({
message: 'Line contains trailing spaces',
lineNumber: 2,
startColumnNumber: 1,
endColumnNumber: 2,
severity: Severity.Warning
})
expect(results).toContainEqual({
message: 'File name contains spaces',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
})
expect(results).toContainEqual({
message: 'File name contains uppercase characters',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
})
expect(results).toContainEqual({
message: 'File missing Doxygen header',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
})
expect(results).toContainEqual({
message: 'Line contains encoded password',
lineNumber: 5,
startColumnNumber: 10,
endColumnNumber: 18,
severity: Severity.Error
})
expect(results).toContainEqual({
message: 'Line is indented with a tab',
lineNumber: 7,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
})
expect(results).toContainEqual({
message: 'Line has incorrect indentation - 3 spaces',
lineNumber: 6,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
})
expect(results.length).toEqual(expectedDiagnostics.length)
expect(results).toContainEqual(expectedDiagnostics[0])
expect(results).toContainEqual(expectedDiagnostics[1])
expect(results).toContainEqual(expectedDiagnostics[2])
expect(results).toContainEqual(expectedDiagnostics[3])
expect(results).toContainEqual(expectedDiagnostics[4])
expect(results).toContainEqual(expectedDiagnostics[5])
expect(results).toContainEqual(expectedDiagnostics[6])
expect(results).toContainEqual(expectedDiagnostics[7])
expect(results).toContainEqual(expectedDiagnostics[8])
})
})

View File

@@ -1,71 +1,135 @@
import { lintFolder } from './lintFolder'
import { Severity } from '../types/Severity'
import path from 'path'
import {
createFile,
createFolder,
deleteFolder,
readFile
} from '@sasjs/utils/file'
const expectedFilesCount = 1
const expectedDiagnostics = [
{
message: 'Line contains trailing spaces',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 2,
severity: Severity.Warning
},
{
message: 'Line contains trailing spaces',
lineNumber: 2,
startColumnNumber: 1,
endColumnNumber: 2,
severity: Severity.Warning
},
{
message: 'File name contains spaces',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
},
{
message: 'File name contains uppercase characters',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
},
{
message: 'File missing Doxygen header',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
},
{
message: 'Line contains encoded password',
lineNumber: 5,
startColumnNumber: 10,
endColumnNumber: 18,
severity: Severity.Error
},
{
message: 'Line is indented with a tab',
lineNumber: 7,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
},
{
message: 'Line has incorrect indentation - 3 spaces',
lineNumber: 6,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
},
{
message: '%mend statement is missing macro name - mf_getuniquelibref',
lineNumber: 17,
startColumnNumber: 3,
endColumnNumber: 9,
severity: Severity.Warning
}
]
describe('lintFolder', () => {
it('should identify lint issues in a given folder', async () => {
const results = await lintFolder(path.join(__dirname, '..'))
expect(results.size).toEqual(1)
const diagnostics = results.get(
await createFolder(path.join(__dirname, 'lint-folder-test'))
const content = await readFile(
path.join(__dirname, '..', 'Example File.sas')
)
await createFile(
path.join(__dirname, 'lint-folder-test', 'Example File.sas'),
content
)
const results = await lintFolder(path.join(__dirname, 'lint-folder-test'))
expect(results.size).toEqual(expectedFilesCount)
const diagnostics = results.get(
path.join(__dirname, 'lint-folder-test', 'Example File.sas')
)!
expect(diagnostics.length).toEqual(8)
expect(diagnostics).toContainEqual({
message: 'Line contains trailing spaces',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 2,
severity: Severity.Warning
})
expect(diagnostics).toContainEqual({
message: 'Line contains trailing spaces',
lineNumber: 2,
startColumnNumber: 1,
endColumnNumber: 2,
severity: Severity.Warning
})
expect(diagnostics).toContainEqual({
message: 'File name contains spaces',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
})
expect(diagnostics).toContainEqual({
message: 'File name contains uppercase characters',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
})
expect(diagnostics).toContainEqual({
message: 'File missing Doxygen header',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
})
expect(diagnostics).toContainEqual({
message: 'Line contains encoded password',
lineNumber: 5,
startColumnNumber: 10,
endColumnNumber: 18,
severity: Severity.Error
})
expect(diagnostics).toContainEqual({
message: 'Line is indented with a tab',
lineNumber: 7,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
})
expect(diagnostics).toContainEqual({
message: 'Line has incorrect indentation - 3 spaces',
lineNumber: 6,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
})
expect(diagnostics.length).toEqual(expectedDiagnostics.length)
expect(diagnostics).toContainEqual(expectedDiagnostics[0])
expect(diagnostics).toContainEqual(expectedDiagnostics[1])
expect(diagnostics).toContainEqual(expectedDiagnostics[2])
expect(diagnostics).toContainEqual(expectedDiagnostics[3])
expect(diagnostics).toContainEqual(expectedDiagnostics[4])
expect(diagnostics).toContainEqual(expectedDiagnostics[5])
expect(diagnostics).toContainEqual(expectedDiagnostics[6])
expect(diagnostics).toContainEqual(expectedDiagnostics[7])
expect(diagnostics).toContainEqual(expectedDiagnostics[8])
await deleteFolder(path.join(__dirname, 'lint-folder-test'))
})
it('should identify lint issues in subfolders of a given folder', async () => {
await createFolder(path.join(__dirname, 'lint-folder-test'))
await createFolder(path.join(__dirname, 'lint-folder-test', 'subfolder'))
const content = await readFile(
path.join(__dirname, '..', 'Example File.sas')
)
await createFile(
path.join(__dirname, 'lint-folder-test', 'subfolder', 'Example File.sas'),
content
)
const results = await lintFolder(path.join(__dirname, 'lint-folder-test'))
expect(results.size).toEqual(expectedFilesCount)
const diagnostics = results.get(
path.join(__dirname, 'lint-folder-test', 'subfolder', 'Example File.sas')
)!
expect(diagnostics.length).toEqual(expectedDiagnostics.length)
expect(diagnostics).toContainEqual(expectedDiagnostics[0])
expect(diagnostics).toContainEqual(expectedDiagnostics[1])
expect(diagnostics).toContainEqual(expectedDiagnostics[2])
expect(diagnostics).toContainEqual(expectedDiagnostics[3])
expect(diagnostics).toContainEqual(expectedDiagnostics[4])
expect(diagnostics).toContainEqual(expectedDiagnostics[5])
expect(diagnostics).toContainEqual(expectedDiagnostics[6])
expect(diagnostics).toContainEqual(expectedDiagnostics[7])
expect(diagnostics).toContainEqual(expectedDiagnostics[8])
await deleteFolder(path.join(__dirname, 'lint-folder-test'))
})
})

View File

@@ -2,76 +2,115 @@ import { lintProject } from './lintProject'
import { Severity } from '../types/Severity'
import * as getProjectRootModule from '../utils/getProjectRoot'
import path from 'path'
import { createFolder, createFile, readFile, deleteFolder } from '@sasjs/utils'
import { DefaultLintConfiguration } from '../utils'
jest.mock('../utils/getProjectRoot')
const expectedFilesCount = 1
const expectedDiagnostics = [
{
message: 'Line contains trailing spaces',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 2,
severity: Severity.Warning
},
{
message: 'Line contains trailing spaces',
lineNumber: 2,
startColumnNumber: 1,
endColumnNumber: 2,
severity: Severity.Warning
},
{
message: 'File name contains spaces',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
},
{
message: 'File name contains uppercase characters',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
},
{
message: 'File missing Doxygen header',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
},
{
message: 'Line contains encoded password',
lineNumber: 5,
startColumnNumber: 10,
endColumnNumber: 18,
severity: Severity.Error
},
{
message: 'Line is indented with a tab',
lineNumber: 7,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
},
{
message: 'Line has incorrect indentation - 3 spaces',
lineNumber: 6,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
},
{
message: '%mend statement is missing macro name - mf_getuniquelibref',
lineNumber: 17,
startColumnNumber: 3,
endColumnNumber: 9,
severity: Severity.Warning
}
]
describe('lintProject', () => {
it('should identify lint issues in a given project', async () => {
await createFolder(path.join(__dirname, 'lint-project-test'))
const content = await readFile(
path.join(__dirname, '..', 'Example File.sas')
)
await createFile(
path.join(__dirname, 'lint-project-test', 'Example File.sas'),
content
)
await createFile(
path.join(__dirname, 'lint-project-test', '.sasjslint'),
JSON.stringify(DefaultLintConfiguration)
)
jest
.spyOn(getProjectRootModule, 'getProjectRoot')
.mockImplementation(() => Promise.resolve(path.join(__dirname, '..')))
.mockImplementation(() =>
Promise.resolve(path.join(__dirname, 'lint-project-test'))
)
const results = await lintProject()
expect(results.size).toEqual(1)
expect(results.size).toEqual(expectedFilesCount)
const diagnostics = results.get(
path.join(__dirname, '..', 'Example File.sas')
path.join(__dirname, 'lint-project-test', 'Example File.sas')
)!
expect(diagnostics.length).toEqual(8)
expect(diagnostics).toContainEqual({
message: 'Line contains trailing spaces',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 2,
severity: Severity.Warning
})
expect(diagnostics).toContainEqual({
message: 'Line contains trailing spaces',
lineNumber: 2,
startColumnNumber: 1,
endColumnNumber: 2,
severity: Severity.Warning
})
expect(diagnostics).toContainEqual({
message: 'File name contains spaces',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
})
expect(diagnostics).toContainEqual({
message: 'File name contains uppercase characters',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
})
expect(diagnostics).toContainEqual({
message: 'File missing Doxygen header',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
})
expect(diagnostics).toContainEqual({
message: 'Line contains encoded password',
lineNumber: 5,
startColumnNumber: 10,
endColumnNumber: 18,
severity: Severity.Error
})
expect(diagnostics).toContainEqual({
message: 'Line is indented with a tab',
lineNumber: 7,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
})
expect(diagnostics).toContainEqual({
message: 'Line has incorrect indentation - 3 spaces',
lineNumber: 6,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
})
expect(diagnostics.length).toEqual(expectedDiagnostics.length)
expect(diagnostics).toContainEqual(expectedDiagnostics[0])
expect(diagnostics).toContainEqual(expectedDiagnostics[1])
expect(diagnostics).toContainEqual(expectedDiagnostics[2])
expect(diagnostics).toContainEqual(expectedDiagnostics[3])
expect(diagnostics).toContainEqual(expectedDiagnostics[4])
expect(diagnostics).toContainEqual(expectedDiagnostics[5])
expect(diagnostics).toContainEqual(expectedDiagnostics[6])
expect(diagnostics).toContainEqual(expectedDiagnostics[7])
expect(diagnostics).toContainEqual(expectedDiagnostics[8])
await deleteFolder(path.join(__dirname, 'lint-project-test'))
})
it('should throw an error when a project root is not found', async () => {

View File

@@ -1,58 +1,58 @@
import { lintFile, lintText } from './lint'
import path from 'path'
/**
* Example which tests a piece of text with all known violations.
*/
const text = `/**
@file
@brief Returns an unused libref
@details Use as follows:
libname mclib0 (work);
libname mclib1 (work);
libname mclib2 (work);
%let libref=%mf_getuniquelibref({SAS001});
%put &=libref;
which returns:
> mclib3
@param prefix= first part of libref. Remember that librefs can only be 8 characters,
so a 7 letter prefix would mean that maxtries should be 10.
@param maxtries= the last part of the libref. Provide an integer value.
@version 9.2
@author Allan Bowe
**/
%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
%local x libref;
%let x={SAS002};
%do x=0 %to &maxtries;
%if %sysfunc(libref(&prefix&x)) ne 0 %then %do;
%let libref=&prefix&x;
%let rc=%sysfunc(libname(&libref,%sysfunc(pathname(work))));
%if &rc %then %put %sysfunc(sysmsg());
&prefix&x
%*put &sysmacroname: Libref &libref assigned as WORK and returned;
%return;
%end;
%end;
%put unable to find available libref in range &prefix.0-&maxtries;
%mend;
`
lintText(text).then((diagnostics) => {
console.log('Text lint results:')
console.table(diagnostics)
})
lintFile(path.join(__dirname, 'Example File.sas')).then((diagnostics) => {
console.log('File lint results:')
console.table(diagnostics)
})
import { lintFile, lintText } from './lint'
import path from 'path'
/**
* Example which tests a piece of text with all known violations.
*/
const text = `/**
@file
@brief Returns an unused libref
@details Use as follows:
libname mclib0 (work);
libname mclib1 (work);
libname mclib2 (work);
%let libref=%mf_getuniquelibref({SAS001});
%put &=libref;
which returns:
> mclib3
@param prefix= first part of libref. Remember that librefs can only be 8 characters,
so a 7 letter prefix would mean that maxtries should be 10.
@param maxtries= the last part of the libref. Provide an integer value.
@version 9.2
@author Allan Bowe
**/
%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
%local x libref;
%let x={SAS002};
%do x=0 %to &maxtries;
%if %sysfunc(libref(&prefix&x)) ne 0 %then %do;
%let libref=&prefix&x;
%let rc=%sysfunc(libname(&libref,%sysfunc(pathname(work))));
%if &rc %then %put %sysfunc(sysmsg());
&prefix&x
%*put &sysmacroname: Libref &libref assigned as WORK and returned;
%return;
%end;
%end;
%put unable to find available libref in range &prefix.0-&maxtries;
%mend;
`
lintText(text).then((diagnostics) => {
console.log('Text lint results:')
console.table(diagnostics)
})
lintFile(path.join(__dirname, 'Example File.sas')).then((diagnostics) => {
console.log('File lint results:')
console.table(diagnostics)
})

View File

@@ -2,7 +2,7 @@ import { LintConfig } from '../../types'
import { Severity } from '../../types/Severity'
import { hasDoxygenHeader } from './hasDoxygenHeader'
describe('hasDoxygenHeader', () => {
describe('hasDoxygenHeader - test', () => {
it('should return an empty array when the file starts with a doxygen header', () => {
const content = `/**
@file
@@ -69,7 +69,9 @@ describe('hasDoxygenHeader', () => {
}
])
})
})
describe('hasDoxygenHeader - fix', () => {
it('should not alter the text if a doxygen header is already present', () => {
const content = `/**
@file
@@ -94,6 +96,7 @@ describe('hasDoxygenHeader', () => {
`/**
@file
@brief <Your brief here>
<h4> SAS Macros </h4>
**/` +
'\n' +
content
@@ -105,7 +108,9 @@ describe('hasDoxygenHeader', () => {
const config = new LintConfig({ lineEndings: 'crlf' })
expect(hasDoxygenHeader.fix!(content, config)).toEqual(
`/**\r\n @file\r\n @brief <Your brief here>\r\n**/` + '\r\n' + content
`/**\r\n @file\r\n @brief <Your brief here>\r\n <h4> SAS Macros </h4>\r\n**/` +
'\r\n' +
content
)
})
})

View File

@@ -4,7 +4,7 @@ import { FileLintRule } from '../../types/LintRule'
import { LintRuleType } from '../../types/LintRuleType'
import { Severity } from '../../types/Severity'
const DoxygenHeader = `/**{lineEnding} @file{lineEnding} @brief <Your brief here>{lineEnding}**/`
const DoxygenHeader = `/**{lineEnding} @file{lineEnding} @brief <Your brief here>{lineEnding} <h4> SAS Macros </h4>{lineEnding}**/`
const name = 'hasDoxygenHeader'
const description =

View File

@@ -1,7 +1,8 @@
import { LintConfig } from '../../types'
import { Severity } from '../../types/Severity'
import { hasMacroNameInMend } from './hasMacroNameInMend'
describe('hasMacroNameInMend', () => {
describe('hasMacroNameInMend - test', () => {
it('should return an empty array when %mend has correct macro name', () => {
const content = `
%macro somemacro();
@@ -319,4 +320,146 @@ describe('hasMacroNameInMend', () => {
})
})
})
it('should use the configured line ending while testing content', () => {
const content = `%macro somemacro();\r\n%put &sysmacroname;\r\n%mend;`
const diagnostics = hasMacroNameInMend.test(
content,
new LintConfig({ lineEndings: 'crlf' })
)
expect(diagnostics).toEqual([
{
message: '%mend statement is missing macro name - somemacro',
lineNumber: 3,
startColumnNumber: 1,
endColumnNumber: 7,
severity: Severity.Warning
}
])
})
})
describe('hasMacroNameInMend - fix', () => {
it('should add macro name to the mend statement if not present', () => {
const content = ` %macro somemacro;\n %put &sysmacroname;\n %mend;`
const expectedContent = ` %macro somemacro;\n %put &sysmacroname;\n %mend somemacro;`
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
expect(formattedContent).toEqual(expectedContent)
})
it('should add macro name to the mend statement if not present ( code in single line )', () => {
const content = `%macro somemacro; %put &sysmacroname; %mend; some code;`
const expectedContent = `%macro somemacro; %put &sysmacroname; %mend somemacro; some code;`
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
expect(formattedContent).toEqual(expectedContent)
})
it('should add macro name to the mend statement if not present ( with multiple macros )', () => {
const content = `
%macro somemacro;
%put &sysmacroname;
%mend somemacro;
%macro somemacro2;
%put &sysmacroname2;
%mend;`
const expectedContent = `
%macro somemacro;
%put &sysmacroname;
%mend somemacro;
%macro somemacro2;
%put &sysmacroname2;
%mend somemacro2;`
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
expect(formattedContent).toEqual(expectedContent)
})
it('should remove redundant %mend statement', () => {
const content = `
%macro somemacro;
%put &sysmacroname;
%mend somemacro;
%mend something;`
const expectedContent = `
%macro somemacro;
%put &sysmacroname;
%mend somemacro;
`
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
expect(formattedContent).toEqual(expectedContent)
})
it('should remove redundant %mend statement with comments', () => {
const content = `
%macro somemacro;
%put &sysmacroname;
%mend somemacro;
/* some comment */
/* some comment */ %mend something; some code;
/* some comment */`
const expectedContent = `
%macro somemacro;
%put &sysmacroname;
%mend somemacro;
/* some comment */
/* some comment */ some code;
/* some comment */`
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
expect(formattedContent).toEqual(expectedContent)
})
it('should correct mismatched macro name', () => {
const content = `
%macro somemacro;
%put &sysmacroname;
%mend someanothermacro;`
const expectedContent = `
%macro somemacro;
%put &sysmacroname;
%mend somemacro;`
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
expect(formattedContent).toEqual(expectedContent)
})
it('should correct mismatched macro name with comments', () => {
const content = `
%macro somemacro;
/* some comments */
%put &sysmacroname;
/* some comments */
%mend someanothermacro ;`
const expectedContent = `
%macro somemacro;
/* some comments */
%put &sysmacroname;
/* some comments */
%mend somemacro ;`
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
expect(formattedContent).toEqual(expectedContent)
})
it('should use the configured line ending while applying the fix', () => {
const content = `%macro somemacro();\r\n%put &sysmacroname;\r\n%mend ;`
const expectedContent = `%macro somemacro();\r\n%put &sysmacroname;\r\n%mend somemacro ;`
const formattedContent = hasMacroNameInMend.fix!(
content,
new LintConfig({ lineEndings: 'crlf' })
)
expect(formattedContent).toEqual(expectedContent)
})
})

View File

@@ -18,17 +18,13 @@ const test = (value: string, config?: LintConfig) => {
const diagnostics: Diagnostic[] = []
macros.forEach((macro) => {
if (macro.startLineNumber === null && macro.endLineNumber !== null) {
const endLine = lines[macro.endLineNumber - 1]
diagnostics.push({
message: `%mend statement is redundant`,
lineNumber: macro.endLineNumber,
startColumnNumber: getColumnNumber(
lines[macro.endLineNumber - 1],
'%mend'
),
startColumnNumber: getColumnNumber(endLine, '%mend'),
endColumnNumber:
getColumnNumber(lines[macro.endLineNumber - 1], '%mend') +
lines[macro.endLineNumber - 1].trim().length -
1,
getColumnNumber(endLine, '%mend') + macro.termination.length,
severity: Severity.Warning
})
} else if (macro.endLineNumber === null && macro.startLineNumber !== null) {
@@ -40,35 +36,29 @@ const test = (value: string, config?: LintConfig) => {
severity: Severity.Warning
})
} else if (macro.mismatchedMendMacroName) {
const endLine = lines[(macro.endLineNumber as number) - 1]
diagnostics.push({
message: `%mend statement has mismatched macro name, it should be '${
macro!.name
}'`,
lineNumber: macro.endLineNumber as number,
startColumnNumber: getColumnNumber(
lines[(macro.endLineNumber as number) - 1],
endLine,
macro.mismatchedMendMacroName
),
endColumnNumber:
getColumnNumber(
lines[(macro.endLineNumber as number) - 1],
macro.mismatchedMendMacroName
) +
getColumnNumber(endLine, macro.mismatchedMendMacroName) +
macro.mismatchedMendMacroName.length -
1,
severity: Severity.Warning
})
} else if (!macro.hasMacroNameInMend) {
const endLine = lines[(macro.endLineNumber as number) - 1]
diagnostics.push({
message: `%mend statement is missing macro name - ${macro.name}`,
lineNumber: macro.endLineNumber as number,
startColumnNumber: getColumnNumber(
lines[(macro.endLineNumber as number) - 1],
'%mend'
),
endColumnNumber:
getColumnNumber(lines[(macro.endLineNumber as number) - 1], '%mend') +
6,
startColumnNumber: getColumnNumber(endLine, '%mend'),
endColumnNumber: getColumnNumber(endLine, '%mend') + 6,
severity: Severity.Warning
})
}
@@ -79,16 +69,52 @@ const test = (value: string, config?: LintConfig) => {
const fix = (value: string, config?: LintConfig): string => {
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
let formattedText = value
const lines: string[] = value ? value.split(lineEnding) : []
const macros = parseMacros(value, config)
macros
.filter((macro) => !macro.hasMacroNameInMend)
.forEach((macro) => {
formattedText = formattedText.replace(
macro.termination,
`%mend ${macro.name};${lineEnding}`
macros.forEach((macro) => {
if (macro.startLineNumber === null && macro.endLineNumber !== null) {
// %mend statement is redundant
const endLine = lines[macro.endLineNumber - 1]
const startColumnNumber = getColumnNumber(endLine, '%mend')
const endColumnNumber =
getColumnNumber(endLine, '%mend') + macro.termination.length
const beforeStatement = endLine.slice(0, startColumnNumber - 1)
const afterStatement = endLine.slice(endColumnNumber)
lines[macro.endLineNumber - 1] = beforeStatement + afterStatement
} else if (macro.endLineNumber === null && macro.startLineNumber !== null) {
// missing %mend statement
} else if (macro.mismatchedMendMacroName) {
// mismatched macro name
const endLine = lines[(macro.endLineNumber as number) - 1]
const startColumnNumber = getColumnNumber(
endLine,
macro.mismatchedMendMacroName
)
})
const endColumnNumber =
getColumnNumber(endLine, macro.mismatchedMendMacroName) +
macro.mismatchedMendMacroName.length -
1
const beforeMacroName = endLine.slice(0, startColumnNumber - 1)
const afterMacroName = endLine.slice(endColumnNumber)
lines[(macro.endLineNumber as number) - 1] =
beforeMacroName + macro.name + afterMacroName
} else if (!macro.hasMacroNameInMend) {
// %mend statement is missing macro name
const endLine = lines[(macro.endLineNumber as number) - 1]
const startColumnNumber = getColumnNumber(endLine, '%mend')
const endColumnNumber = getColumnNumber(endLine, '%mend') + 4
const beforeStatement = endLine.slice(0, startColumnNumber - 1)
const afterStatement = endLine.slice(endColumnNumber)
lines[(macro.endLineNumber as number) - 1] =
beforeStatement + `%mend ${macro.name}` + afterStatement
}
})
const formattedText = lines.join(lineEnding)
return formattedText
}

View File

@@ -44,6 +44,21 @@ describe('hasMacroParentheses', () => {
])
})
it('should return an array with a single diagnostic when macro defined without name ( single line code )', () => {
const content = `
%macro (); %put &sysmacroname; %mend;`
expect(hasMacroParentheses.test(content)).toEqual([
{
message: 'Macro definition missing name',
lineNumber: 2,
startColumnNumber: 3,
endColumnNumber: 12,
severity: Severity.Warning
}
])
})
it('should return an array with a single diagnostic when macro defined without name and parentheses', () => {
const content = `
%macro ;
@@ -55,7 +70,7 @@ describe('hasMacroParentheses', () => {
message: 'Macro definition missing name',
lineNumber: 2,
startColumnNumber: 3,
endColumnNumber: 10,
endColumnNumber: 9,
severity: Severity.Warning
}
])

View File

@@ -17,17 +17,19 @@ const test = (value: string, config?: LintConfig) => {
diagnostics.push({
message: 'Macro definition missing name',
lineNumber: macro.startLineNumber!,
startColumnNumber: getColumnNumber(macro.declaration, '%macro'),
endColumnNumber: macro.declaration.length,
startColumnNumber: getColumnNumber(macro.declarationLine, '%macro'),
endColumnNumber:
getColumnNumber(macro.declarationLine, '%macro') +
macro.declaration.length,
severity: Severity.Warning
})
} else if (!macro.declaration.includes('(')) {
} else if (!macro.declarationLine.includes('(')) {
diagnostics.push({
message,
lineNumber: macro.startLineNumber!,
startColumnNumber: getColumnNumber(macro.declaration, macro.name),
startColumnNumber: getColumnNumber(macro.declarationLine, macro.name),
endColumnNumber:
getColumnNumber(macro.declaration, macro.name) +
getColumnNumber(macro.declarationLine, macro.name) +
macro.name.length -
1,
severity: Severity.Warning
@@ -36,9 +38,9 @@ const test = (value: string, config?: LintConfig) => {
diagnostics.push({
message: 'Macro definition contains space(s)',
lineNumber: macro.startLineNumber!,
startColumnNumber: getColumnNumber(macro.declaration, macro.name),
startColumnNumber: getColumnNumber(macro.declarationLine, macro.name),
endColumnNumber:
getColumnNumber(macro.declaration, macro.name) +
getColumnNumber(macro.declarationLine, macro.name) +
macro.name.length -
1 +
`()`.length,

View File

@@ -2,7 +2,7 @@ import { LintConfig, Severity } from '../../types'
import { LineEndings } from '../../types/LineEndings'
import { lineEndings } from './lineEndings'
describe('lineEndings', () => {
describe('lineEndings - test', () => {
it('should return an empty array when the text contains the configured line endings', () => {
const text = "%put 'hello';\n%put 'world';\n"
const config = new LintConfig({ lineEndings: LineEndings.LF })
@@ -101,7 +101,9 @@ describe('lineEndings', () => {
severity: Severity.Warning
})
})
})
describe('lineEndings - fix', () => {
it('should transform line endings to LF', () => {
const text =
"%put 'hello';\r\n%put 'test';\r\n%put 'world';\n%put 'test2';\n%put 'world2';\r\n"

View File

@@ -1,3 +1,4 @@
import { LintConfig } from '../../types'
import { Severity } from '../../types/Severity'
import { noNestedMacros } from './noNestedMacros'
@@ -73,4 +74,23 @@ describe('noNestedMacros', () => {
expect(noNestedMacros.test((content as unknown) as string)).toEqual([])
})
it('should use the configured line ending while testing content', () => {
const content = `%macro outer();\r\n%macro inner;\r\n%mend inner;\r\n%mend outer;`
const diagnostics = noNestedMacros.test(
content,
new LintConfig({ lineEndings: 'crlf' })
)
expect(diagnostics).toEqual([
{
message: "Macro definition for 'inner' present in macro 'outer'",
lineNumber: 2,
startColumnNumber: 1,
endColumnNumber: 13,
severity: Severity.Warning
}
])
})
})

View File

@@ -2,6 +2,10 @@ import * as fileModule from '@sasjs/utils/file'
import { LintConfig } from '../types/LintConfig'
import { getLintConfig } from './getLintConfig'
const expectedFileLintRulesCount = 4
const expectedLineLintRulesCount = 5
const expectedPathLintRulesCount = 2
describe('getLintConfig', () => {
it('should get the lint config', async () => {
const config = await getLintConfig()
@@ -17,8 +21,8 @@ describe('getLintConfig', () => {
const config = await getLintConfig()
expect(config).toBeInstanceOf(LintConfig)
expect(config.fileLintRules.length).toEqual(3)
expect(config.lineLintRules.length).toEqual(5)
expect(config.pathLintRules.length).toEqual(2)
expect(config.fileLintRules.length).toEqual(expectedFileLintRulesCount)
expect(config.lineLintRules.length).toEqual(expectedLineLintRulesCount)
expect(config.pathLintRules.length).toEqual(expectedPathLintRulesCount)
})
})

View File

@@ -15,7 +15,7 @@ export const DefaultLintConfiguration = {
maxLineLength: 80,
noTabIndentation: true,
indentationMultiple: 2,
hasMacroNameInMend: false,
hasMacroNameInMend: true,
noNestedMacros: true,
hasMacroParentheses: true
}

View File

@@ -12,7 +12,9 @@ describe('parseMacros', () => {
expect(macros.length).toEqual(1)
expect(macros).toContainEqual({
name: 'test',
declaration: '%macro test;',
declarationLine: '%macro test;',
terminationLine: '%mend',
declaration: '%macro test',
termination: '%mend',
startLineNumber: 1,
endLineNumber: 3,
@@ -36,8 +38,10 @@ describe('parseMacros', () => {
expect(macros.length).toEqual(2)
expect(macros).toContainEqual({
name: 'foo',
declaration: '%macro foo;',
termination: '%mend;',
declarationLine: '%macro foo;',
terminationLine: '%mend;',
declaration: '%macro foo',
termination: '%mend',
startLineNumber: 1,
endLineNumber: 3,
parentMacro: '',
@@ -47,8 +51,10 @@ describe('parseMacros', () => {
})
expect(macros).toContainEqual({
name: 'bar',
declaration: '%macro bar();',
termination: '%mend bar;',
declarationLine: '%macro bar();',
terminationLine: '%mend bar;',
declaration: '%macro bar()',
termination: '%mend bar',
startLineNumber: 4,
endLineNumber: 6,
parentMacro: '',
@@ -71,6 +77,8 @@ describe('parseMacros', () => {
expect(macros.length).toEqual(2)
expect(macros).toContainEqual({
name: 'test',
declarationLine: '%macro test()',
terminationLine: '%mend test',
declaration: '%macro test()',
termination: '%mend test',
startLineNumber: 1,
@@ -82,8 +90,10 @@ describe('parseMacros', () => {
})
expect(macros).toContainEqual({
name: 'test2',
declaration: ' %macro test2',
termination: ' %mend',
declarationLine: ' %macro test2',
terminationLine: ' %mend',
declaration: '%macro test2',
termination: '%mend',
startLineNumber: 3,
endLineNumber: 5,
parentMacro: 'test',

View File

@@ -6,6 +6,8 @@ interface Macro {
name: string
startLineNumber: number | null
endLineNumber: number | null
declarationLine: string
terminationLine: string
declaration: string
termination: string
parentMacro: string
@@ -52,7 +54,9 @@ export const parseMacros = (text: string, config?: LintConfig): Macro[] => {
hasParentheses: trimmedStatement.endsWith('()'),
hasMacroNameInMend: false,
mismatchedMendMacroName: '',
declaration: line,
declarationLine: line,
terminationLine: '',
declaration: trimmedStatement,
termination: ''
})
} else if (trimmedStatement.startsWith('%mend')) {
@@ -61,11 +65,12 @@ export const parseMacros = (text: string, config?: LintConfig): Macro[] => {
const mendMacroName =
trimmedStatement.split(' ').filter((s: string) => !!s)[1] || ''
macro.endLineNumber = index + 1
macro.hasMacroNameInMend = trimmedStatement.includes(macro.name)
macro.hasMacroNameInMend = mendMacroName === macro.name
macro.mismatchedMendMacroName = macro.hasMacroNameInMend
? ''
: mendMacroName
macro.termination = line
macro.terminationLine = line
macro.termination = trimmedStatement
macros.push(macro)
} else {
macros.push({
@@ -76,8 +81,10 @@ export const parseMacros = (text: string, config?: LintConfig): Macro[] => {
hasParentheses: false,
hasMacroNameInMend: false,
mismatchedMendMacroName: '',
declarationLine: '',
terminationLine: line,
declaration: '',
termination: line
termination: trimmedStatement
})
}
}