1
0
mirror of https://github.com/sasjs/lint.git synced 2025-12-10 17:34:36 +00:00

Compare commits

..

30 Commits

Author SHA1 Message Date
Muhammad Saad
7ccb122744 Merge pull request #80 from sasjs/postinstall-fix
fix: postinstall -> prepare, support windows CMD/Powershell
2021-07-05 01:14:20 +05:00
Saad Jutt
884480d3df fix: postinstall -> prepare, support windows CMD/Powershell 2021-07-05 01:05:41 +05:00
Allan Bowe
1b940497aa Update README.md 2021-06-25 22:30:32 +03:00
Allan Bowe
94d9d246eb Merge pull request #71 from sasjs/doxygen-header-enforces-double-asterisks
fix: doxygen header enforces double asterisks
2021-06-15 10:41:18 +03:00
Saad Jutt
95502647e8 fix: doxygen header requires to only start with double asterisks 2021-06-14 18:16:05 +05:00
Saad Jutt
be9d5b8e68 fix: doxygen header enforces double asterisks 2021-06-14 18:06:37 +05:00
Krishna Acondy
c2d368327b Merge pull request #68 from sasjs/dependabot/npm_and_yarn/sasjs/utils-2.19.0
chore(deps): bump @sasjs/utils from 2.18.0 to 2.19.0
2021-06-10 11:39:14 +01:00
dependabot[bot]
94a693e57d chore(deps): bump @sasjs/utils from 2.18.0 to 2.19.0
Bumps [@sasjs/utils](https://github.com/sasjs/utils) from 2.18.0 to 2.19.0.
- [Release notes](https://github.com/sasjs/utils/releases)
- [Commits](https://github.com/sasjs/utils/compare/v2.18.0...v2.19.0)

---
updated-dependencies:
- dependency-name: "@sasjs/utils"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-10 06:31:08 +00:00
Krishna Acondy
fec3372f92 Merge pull request #66 from sasjs/dependabot/npm_and_yarn/types/node-15.12.2
chore(deps-dev): bump @types/node from 15.0.2 to 15.12.2
2021-06-09 08:43:14 +01:00
dependabot[bot]
d5b38373d4 chore(deps-dev): bump @types/node from 15.0.2 to 15.12.2
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 15.0.2 to 15.12.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-09 07:41:42 +00:00
Krishna Acondy
21114e0a6f Merge pull request #59 from sasjs/dependabot/npm_and_yarn/typescript-4.3.2
chore(deps-dev): bump typescript from 4.2.4 to 4.3.2
2021-06-09 08:39:09 +01:00
dependabot[bot]
b52b3ac42f chore(deps-dev): bump typescript from 4.2.4 to 4.3.2
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.2.4 to 4.3.2.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.2.4...v4.3.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-09 07:24:53 +00:00
Krishna Acondy
7f4c389468 Merge pull request #67 from sasjs/dependabot/npm_and_yarn/sasjs/utils-2.18.0
chore(deps): bump @sasjs/utils from 2.12.0 to 2.18.0
2021-06-09 08:22:30 +01:00
dependabot[bot]
1fd4cd7ddc chore(deps): bump @sasjs/utils from 2.12.0 to 2.18.0
Bumps [@sasjs/utils](https://github.com/sasjs/utils) from 2.12.0 to 2.18.0.
- [Release notes](https://github.com/sasjs/utils/releases)
- [Commits](https://github.com/sasjs/utils/compare/v2.12.0...v2.18.0)

---
updated-dependencies:
- dependency-name: "@sasjs/utils"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-09 07:19:50 +00:00
Krishna Acondy
b13302a315 Merge pull request #55 from sasjs/macro-declaration-multi-line
feat: mult-line macro declarations
2021-06-09 08:17:41 +01:00
Krishna Acondy
0790a447f3 Merge branch 'main' into macro-declaration-multi-line 2021-06-09 08:16:42 +01:00
Krishna Acondy
11182aaaa7 Merge pull request #46 from sasjs/dependabot/npm_and_yarn/ts-jest-26.5.6
chore(deps-dev): bump ts-jest from 26.5.5 to 26.5.6
2021-06-09 08:16:26 +01:00
Krishna Acondy
7144d0cfe3 Merge branch 'main' into dependabot/npm_and_yarn/ts-jest-26.5.6 2021-06-09 08:15:17 +01:00
Saad Jutt
0caf31b7ff chore: Code Refactor 2021-05-22 00:08:24 +05:00
Saad Jutt
020a1e08d0 chore: prevent space b/w macroName and starting parentheses is not applicable 2021-05-21 20:16:24 +05:00
Saad Jutt
a762dadf37 chore: testing content into single line 2021-05-21 20:08:38 +05:00
Saad Jutt
c9fa366130 fix(parseMacros): avoid statement break on html encoded semi colon 2021-05-21 20:05:53 +05:00
Saad Jutt
5701064c07 fix(strictMacroDefinition): updated logic for getting options 2021-05-21 19:41:45 +05:00
Saad Jutt
cbfa1f40d1 fix(strictMacroDefinition): updated logic for getting params 2021-05-21 18:32:22 +05:00
Saad Jutt
d391a4e8fc fix(trimComments): handle case special comment case 2021-05-21 18:05:22 +05:00
Saad Jutt
f793eb3a76 fix(strictMacroDefinition): moved from lineRules to fileRules 2021-05-21 17:29:23 +05:00
Saad Jutt
af2d2c12c1 feat: mult-line macro declarations 2021-05-21 05:10:34 +05:00
Saad Jutt
8bfb547427 chore: removed redundant property from parsedMacros 2021-05-20 23:00:59 +05:00
Saad Jutt
d7721f8e5e fix: comments within/outside the statement 2021-05-20 22:41:16 +05:00
dependabot[bot]
021f36663a chore(deps-dev): bump ts-jest from 26.5.5 to 26.5.6
Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 26.5.5 to 26.5.6.
- [Release notes](https://github.com/kulshekhar/ts-jest/releases)
- [Changelog](https://github.com/kulshekhar/ts-jest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kulshekhar/ts-jest/compare/v26.5.5...v26.5.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-20 07:00:31 +00:00
25 changed files with 871 additions and 344 deletions

View File

@@ -44,7 +44,7 @@ The SASjs framework recommends the use of Doxygen headers for describing all typ
* Severity: WARNING
#### hasMacroNameInMend
The addition of the macro name in the `%mend` statement is optional, but can approve readability in large programs. A discussion on this topic can be found [here](https://www.linkedin.com/posts/allanbowe_sas-sasapps-sasjs-activity-6783413360781266945-1-7m). The default setting will be the result of a popular vote by around 300 people.
The addition of the macro name in the `%mend` statement is optional, but can approve readability in large programs. A discussion on this topic can be found [here](https://www.linkedin.com/posts/allanbowe_sas-sasapps-sasjs-activity-6783413360781266945-1-7m). The default setting was the result of a poll with over 300 votes.
* Default: true
* Severity: WARNING
@@ -72,13 +72,13 @@ Code becomes far more readable when line lengths are short. The most compelling
In batch mode, long SAS code lines may also be truncated, causing hard-to-detect errors.
For this reason we strongly recommend a line length limit, and we set the bar at 80. To turn this feature off, set the value to 0.
We strongly recommend a line length limit, and set the bar at 80. To turn this feature off, set the value to 0.
* Default: 80
* Severity: WARNING
#### noNestedMacros
Where macros are defined inside other macros, they are recompiled every time the outer maro is invoked. Hence, it is widely considered inefficient, and bad practice, to nest macro definitions.
Where macros are defined inside other macros, they are recompiled every time the outer macro is invoked. Hence, it is widely considered inefficient, and bad practice, to nest macro definitions.
* Default: true
* Severity: WARNING
@@ -95,7 +95,7 @@ In addition, when such files are used in URLs, they are often padded with a mess
* Severity: WARNING
#### noTabIndentation
Whilst there are some arguments for using tabs to indent (such as the ability to set your own indentation width, and to save on characters) there are many, many, many developers who think otherwise. We're in that camp. Sorry (not sorry).
Whilst there are some arguments for using tabs to indent (such as the ability to set your own indentation width, and to reduce character count) there are many, many, many developers who think otherwise. We're in that camp. Sorry (not sorry).
* Default: true
* Severity: WARNING
@@ -116,14 +116,19 @@ This will highlight lines with trailing spaces. Trailing spaces serve no useful
A formatter will automatically apply rules when you hit SAVE, which can save a LOT of time.
We're looking to implement the following rules:
We've already implemented the following rules:
* Remove trailing spaces
* Change tabs to spaces
* Add the macro name to the %mend statement
* Add a doxygen header template if none exists
* Remove trailing spaces
Later we will investigate some harder stuff, such as automatic indentation and code layout
We're looking to implement the following rules:
* Change tabs to spaces
* zap gremlins
* fix line endings
We are also investigating some harder stuff, such as automatic indentation and code layout
## Sponsorship & Contributions
@@ -133,7 +138,7 @@ Contact [Allan Bowe](https://www.linkedin.com/in/allanbowe/) for further details
## SAS 9 Health check
The SASjs Linter (and formatter) is a great way to de-risk and accelerate the delivery of SAS code into production environments. However, code is just one part of a SAS estate. If you are running SAS 9, you may be interested to know what 'gremlins' are lurking in your system. Maybe you are preparing for a migration. Maybe you are preparing to hand over the control of your environment. Either way, an assessment of your existing system would put minds at rest and pro-actively identify trouble spots.
The SASjs Linter (and formatter) is a great way to de-risk and accelerate the delivery of SAS code into production environments. However, code is just one part of a SAS estate. If you are running SAS 9, you may be interested to know what 'gremlins' are lurking in your SAS 9 system. Maybe you are preparing for a migration. Maybe you are preparing to hand over the control of your environment. Either way, an assessment of your existing system would put minds at rest and pro-actively identify trouble spots.
The SAS 9 Health Check is a 'plug & play' product, that uses the [SAS 9 REST API](https://sas9api.io) to run hundreds of metadata and system checks to identify common problems. The checks are non-invasive, and becuase it is a client app, there is NOTHING TO INSTALL on your SAS server. We offer this assessment for a low fixed fee, and if you engage our (competitively priced) services to address the issues we highlight, then the assessment is free.

71
package-lock.json generated
View File

@@ -648,13 +648,15 @@
}
},
"@sasjs/utils": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.12.0.tgz",
"integrity": "sha512-OnC/7R+nGI8tlSPCcI7fPyD7T97B+McnkXT0IuAYDNGbfwRPuseWq0I1h+kbAWThGT67H4hnp61N0qr8LkpHZQ==",
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.19.0.tgz",
"integrity": "sha512-b/NlIvTaISIFsllucetBwJjFUiM13I+bS06WtK3WN0G1EXzVrjJt+eXqgkQZbIZfoaeKo5oRigOtZUXth65duQ==",
"requires": {
"@types/prompts": "^2.0.13",
"chalk": "^4.1.1",
"cli-table": "^0.3.6",
"consola": "^2.15.0",
"fs-extra": "^10.0.0",
"prompts": "^2.4.1",
"valid-url": "^1.0.9"
},
@@ -782,10 +784,9 @@
}
},
"@types/node": {
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz",
"integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==",
"dev": true
"version": "15.12.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz",
"integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww=="
},
"@types/normalize-package-data": {
"version": "2.4.0",
@@ -799,6 +800,14 @@
"integrity": "sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA==",
"dev": true
},
"@types/prompts": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.0.13.tgz",
"integrity": "sha512-jwMOIGy49VruR/gYehhJYgpVzB+EVpEE7t7j9m1oTo4HMpOe7KmsyqdBuoxAzA5B4caUgx0cKrWr7wUEqMXJ7Q==",
"requires": {
"@types/node": "*"
}
},
"@types/stack-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
@@ -1938,6 +1947,23 @@
"map-cache": "^0.2.2"
}
},
"fs-extra": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz",
"integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"dependencies": {
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
}
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -2022,8 +2048,7 @@
"graceful-fs": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
"integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==",
"dev": true
"integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ=="
},
"growly": {
"version": "1.3.0",
@@ -3055,6 +3080,22 @@
"minimist": "^1.2.5"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
},
"dependencies": {
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
}
}
},
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@@ -4540,9 +4581,9 @@
}
},
"ts-jest": {
"version": "26.5.5",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.5.tgz",
"integrity": "sha512-7tP4m+silwt1NHqzNRAPjW1BswnAhopTdc2K3HEkRZjF0ZG2F/e/ypVH0xiZIMfItFtD3CX0XFbwPzp9fIEUVg==",
"version": "26.5.6",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.6.tgz",
"integrity": "sha512-rua+rCP8DxpA8b4DQD/6X2HQS8Zy/xzViVYfEs2OQu68tkCuKLV0Md8pmX55+W24uRIyAsf/BajRfxOs+R2MKA==",
"dev": true,
"requires": {
"bs-logger": "0.x",
@@ -4620,9 +4661,9 @@
}
},
"typescript": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz",
"integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==",
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.2.tgz",
"integrity": "sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw==",
"dev": true
},
"union-value": {

View File

@@ -9,7 +9,7 @@
"package:lib": "npm run build && cp ./package.json build && cp README.md build && cd build && npm version \"5.0.0\" && npm pack",
"lint:fix": "npx prettier --write '{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
"lint": "npx prettier --check '{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
"postinstall": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true"
"prepare": "git rev-parse --git-dir && git config core.hooksPath ./.git-hooks || true"
},
"publishConfig": {
"access": "public"
@@ -38,13 +38,13 @@
"homepage": "https://github.com/sasjs/lint#readme",
"devDependencies": {
"@types/jest": "^26.0.23",
"@types/node": "^15.0.2",
"@types/node": "^15.12.2",
"jest": "^26.6.3",
"rimraf": "^3.0.2",
"ts-jest": "^26.5.5",
"typescript": "^4.2.4"
"ts-jest": "^26.5.6",
"typescript": "^4.3.2"
},
"dependencies": {
"@sasjs/utils": "^2.12.0"
"@sasjs/utils": "^2.19.0"
}
}

View File

@@ -12,14 +12,14 @@ describe('formatText', () => {
new LintConfig(getLintConfigModule.DefaultLintConfiguration)
)
)
const text = `%macro test
const text = `%macro test;
%put 'hello';\r\n%mend; `
const expectedOutput = `/**
@file
@brief <Your brief here>
<h4> SAS Macros </h4>
**/\n%macro test
**/\n%macro test;
%put 'hello';\n%mend test;`
const output = await formatText(text)
@@ -38,9 +38,9 @@ describe('formatText', () => {
})
)
)
const text = `%macro test\n %put 'hello';\r\n%mend; `
const text = `%macro test;\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 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)

View File

@@ -17,6 +17,32 @@ describe('hasDoxygenHeader - test', () => {
expect(hasDoxygenHeader.test(content)).toEqual([])
})
it('should return an empty array when the file starts with a doxygen header', () => {
const content = `
/*
@file
@brief Returns an unused libref
*/
%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
%local x libref;
%let x={SAS002};
%do x=0 %to &maxtries;`
expect(hasDoxygenHeader.test(content)).toEqual([
{
message:
'File not following Doxygen header style, use double asterisks',
lineNumber: 4,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
}
])
})
it('should return an array with a single diagnostic when the file has no header', () => {
const content = `
%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
@@ -86,7 +112,33 @@ describe('hasDoxygenHeader - fix', () => {
expect(hasDoxygenHeader.fix!(content)).toEqual(content)
})
it('should should add a doxygen header if not present', () => {
it('should update single asterisks to double if a doxygen header is already present', () => {
const contentOriginal = `
/*
@file
@brief Returns an unused libref
*/
%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
%local x libref;
%let x={SAS002};
%do x=0 %to &maxtries;`
const contentExpected = `
/**
@file
@brief Returns an unused libref
*/
%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
%local x libref;
%let x={SAS002};
%do x=0 %to &maxtries;`
expect(hasDoxygenHeader.fix!(contentOriginal)).toEqual(contentExpected)
})
it('should add a doxygen header if not present', () => {
const content = `%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
%local x libref;
%let x={SAS002};

View File

@@ -10,10 +10,28 @@ const name = 'hasDoxygenHeader'
const description =
'Enforce the presence of a Doxygen header at the start of each file.'
const message = 'File missing Doxygen header'
const test = (value: string) => {
const messageForSingleAsterisk =
'File not following Doxygen header style, use double asterisks'
const test = (value: string, config?: LintConfig) => {
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
try {
const hasFileHeader = value.trimStart().startsWith('/*')
const hasFileHeader = value.trimStart().startsWith('/**')
if (hasFileHeader) return []
const hasFileHeaderWithSingleAsterisk = value.trimStart().startsWith('/*')
if (hasFileHeaderWithSingleAsterisk)
return [
{
message: messageForSingleAsterisk,
lineNumber:
(value.split('/*')![0]!.match(new RegExp(lineEnding, 'g')) ?? [])
.length + 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
}
]
return [
{
message,
@@ -37,9 +55,12 @@ const test = (value: string) => {
}
const fix = (value: string, config?: LintConfig): string => {
if (test(value).length === 0) {
const result = test(value, config)
if (result.length === 0) {
return value
}
} else if (result[0].message == messageForSingleAsterisk)
return value.replace('/*', '/**')
const lineEndingConfig = config?.lineEndings || LineEndings.LF
const lineEnding = lineEndingConfig === LineEndings.LF ? '\n' : '\r\n'

View File

@@ -56,7 +56,7 @@ describe('hasMacroNameInMend - test', () => {
it('should return an array with a diagnostic for each macro missing an %mend statement', () => {
const content = `%macro somemacro;
%put &sysmacroname;
%macro othermacro`
%macro othermacro;`
expect(hasMacroNameInMend.test(content)).toEqual([
{

View File

@@ -17,7 +17,7 @@ const test = (value: string, config?: LintConfig) => {
const macros = parseMacros(value, config)
const diagnostics: Diagnostic[] = []
macros.forEach((macro) => {
if (macro.startLineNumber === null && macro.endLineNumber !== null) {
if (macro.startLineNumbers.length === 0 && macro.endLineNumber !== null) {
const endLine = lines[macro.endLineNumber - 1]
diagnostics.push({
message: `%mend statement is redundant`,
@@ -27,10 +27,13 @@ const test = (value: string, config?: LintConfig) => {
getColumnNumber(endLine, '%mend') + macro.termination.length,
severity: Severity.Warning
})
} else if (macro.endLineNumber === null && macro.startLineNumber !== null) {
} else if (
macro.endLineNumber === null &&
macro.startLineNumbers.length !== 0
) {
diagnostics.push({
message: `Missing %mend statement for macro - ${macro.name}`,
lineNumber: macro.startLineNumber,
lineNumber: macro.startLineNumbers![0],
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
@@ -73,7 +76,7 @@ const fix = (value: string, config?: LintConfig): string => {
const macros = parseMacros(value, config)
macros.forEach((macro) => {
if (macro.startLineNumber === null && macro.endLineNumber !== null) {
if (macro.startLineNumbers.length === 0 && macro.endLineNumber !== null) {
// %mend statement is redundant
const endLine = lines[macro.endLineNumber - 1]
const startColumnNumber = getColumnNumber(endLine, '%mend')
@@ -83,7 +86,10 @@ const fix = (value: string, config?: LintConfig): string => {
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) {
} else if (
macro.endLineNumber === null &&
macro.startLineNumbers.length !== 0
) {
// missing %mend statement
} else if (macro.mismatchedMendMacroName) {
// mismatched macro name

View File

@@ -139,18 +139,4 @@ 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

@@ -16,36 +16,36 @@ const test = (value: string, config?: LintConfig) => {
if (!macro.name) {
diagnostics.push({
message: 'Macro definition missing name',
lineNumber: macro.startLineNumber!,
startColumnNumber: getColumnNumber(macro.declarationLine, '%macro'),
lineNumber: macro.startLineNumbers![0],
startColumnNumber: getColumnNumber(
macro.declarationLines![0],
'%macro'
),
endColumnNumber:
getColumnNumber(macro.declarationLine, '%macro') +
getColumnNumber(macro.declarationLines![0], '%macro') +
macro.declaration.length,
severity: Severity.Warning
})
} else if (!macro.declarationLine.includes('(')) {
} else if (!macro.declarationLines.find((dl) => dl.includes('('))) {
const macroNameLineIndex = macro.declarationLines.findIndex((dl) =>
dl.includes(macro.name)
)
diagnostics.push({
message,
lineNumber: macro.startLineNumber!,
startColumnNumber: getColumnNumber(macro.declarationLine, macro.name),
lineNumber: macro.startLineNumbers![macroNameLineIndex],
startColumnNumber: getColumnNumber(
macro.declarationLines[macroNameLineIndex],
macro.name
),
endColumnNumber:
getColumnNumber(macro.declarationLine, macro.name) +
getColumnNumber(
macro.declarationLines[macroNameLineIndex],
macro.name
) +
macro.name.length -
1,
severity: Severity.Warning
})
} else if (macro.name !== macro.name.trim()) {
diagnostics.push({
message: 'Macro definition contains space(s)',
lineNumber: macro.startLineNumber!,
startColumnNumber: getColumnNumber(macro.declarationLine, macro.name),
endColumnNumber:
getColumnNumber(macro.declarationLine, macro.name) +
macro.name.length -
1 +
`()`.length,
severity: Severity.Warning
})
}
})

View File

@@ -3,3 +3,4 @@ export { hasMacroNameInMend } from './hasMacroNameInMend'
export { hasMacroParentheses } from './hasMacroParentheses'
export { lineEndings } from './lineEndings'
export { noNestedMacros } from './noNestedMacros'
export { strictMacroDefinition } from './strictMacroDefinition'

View File

@@ -22,17 +22,17 @@ const test = (value: string, config?: LintConfig) => {
message: message
.replace('{macro}', macro.name)
.replace('{parent}', macro.parentMacro),
lineNumber: macro.startLineNumber as number,
lineNumber: macro.startLineNumbers![0] as number,
startColumnNumber: getColumnNumber(
lines[(macro.startLineNumber as number) - 1],
lines[(macro.startLineNumbers![0] as number) - 1],
'%macro'
),
endColumnNumber:
getColumnNumber(
lines[(macro.startLineNumber as number) - 1],
lines[(macro.startLineNumbers![0] as number) - 1],
'%macro'
) +
lines[(macro.startLineNumber as number) - 1].trim().length -
lines[(macro.startLineNumbers![0] as number) - 1].trim().length -
1,
severity: Severity.Warning
})

View File

@@ -0,0 +1,216 @@
import { LintConfig, Severity } from '../../types'
import { strictMacroDefinition } from './strictMacroDefinition'
describe('strictMacroDefinition', () => {
it('should return an empty array when the content has correct macro definition syntax', () => {
const content = '%macro somemacro;'
expect(strictMacroDefinition.test(content)).toEqual([])
const content2 = '%macro somemacro();'
expect(strictMacroDefinition.test(content2)).toEqual([])
const content3 = '%macro somemacro(var1);'
expect(strictMacroDefinition.test(content3)).toEqual([])
const content4 = '%macro somemacro/minoperator;'
expect(strictMacroDefinition.test(content4)).toEqual([])
const content5 = '%macro somemacro /minoperator;'
expect(strictMacroDefinition.test(content5)).toEqual([])
const content6 = '%macro somemacro(var1, var2)/minoperator;'
expect(strictMacroDefinition.test(content6)).toEqual([])
const content7 =
' /* Some Comment */ %macro somemacro(var1, var2) /minoperator ; /* Some Comment */'
expect(strictMacroDefinition.test(content7)).toEqual([])
const content8 =
'%macro macroName( arr, arr/* / store source */3 ) /* / store source */;/* / store source */'
expect(strictMacroDefinition.test(content8)).toEqual([])
const content9 = '%macro macroName(var1, var2=with space, var3=);'
expect(strictMacroDefinition.test(content9)).toEqual([])
const content10 = '%macro macroName()/ /* some comment */ store source;'
expect(strictMacroDefinition.test(content10)).toEqual([])
const content11 = '`%macro macroName() /* / store source */;'
expect(strictMacroDefinition.test(content11)).toEqual([])
const content12 =
'%macro macroName()/ /* some comment */ store des="some description";'
expect(strictMacroDefinition.test(content12)).toEqual([])
})
it('should return an array with a single diagnostic when Macro definition has space in param', () => {
const content = '%macro somemacro(va r1);'
expect(strictMacroDefinition.test(content)).toEqual([
{
message: `Param 'va r1' cannot have space`,
lineNumber: 1,
startColumnNumber: 18,
endColumnNumber: 22,
severity: Severity.Warning
}
])
})
it('should return an array with a two diagnostics when Macro definition has space in params', () => {
const content = '%macro somemacro(var1, var 2, v ar3, var4);'
expect(strictMacroDefinition.test(content)).toEqual([
{
message: `Param 'var 2' cannot have space`,
lineNumber: 1,
startColumnNumber: 24,
endColumnNumber: 28,
severity: Severity.Warning
},
{
message: `Param 'v ar3' cannot have space`,
lineNumber: 1,
startColumnNumber: 31,
endColumnNumber: 35,
severity: Severity.Warning
}
])
})
it('should return an array with a two diagnostics when Macro definition has space in params - special case', () => {
const content =
'%macro macroName( arr, ar r/* / store source */ 3 ) /* / store source */;/* / store source */'
expect(strictMacroDefinition.test(content)).toEqual([
{
message: `Param 'ar r 3' cannot have space`,
lineNumber: 1,
startColumnNumber: 24,
endColumnNumber: 49,
severity: Severity.Warning
}
])
})
it('should return an array with a single diagnostic when Macro definition has invalid option', () => {
const content = '%macro somemacro(var1, var2)/minXoperator;'
expect(strictMacroDefinition.test(content)).toEqual([
{
message: `Option 'minXoperator' is not valid`,
lineNumber: 1,
startColumnNumber: 30,
endColumnNumber: 41,
severity: Severity.Warning
}
])
})
it('should return an array with a two diagnostics when Macro definition has invalid options', () => {
const content =
'%macro somemacro(var1, var2)/ store invalidoption secure ;'
expect(strictMacroDefinition.test(content)).toEqual([
{
message: `Option 'invalidoption' is not valid`,
lineNumber: 1,
startColumnNumber: 39,
endColumnNumber: 51,
severity: Severity.Warning
}
])
})
describe('multi-content macro declarations', () => {
it('should return an empty array when the content has correct macro definition syntax', () => {
const content = `%macro mp_ds2cards(base_ds=, tgt_ds=\n ,cards_file="%sysfunc(pathname(work))/cardgen.sas"\n ,maxobs=max\n ,random_sample=NO\n ,showlog=YES\n ,outencoding=\n ,append=NO\n)/*/STORE SOURCE*/;`
expect(strictMacroDefinition.test(content)).toEqual([])
const content2 = `%macro mm_createapplication(\n tree=/User Folders/sasdemo\n ,name=myApp\n ,ClassIdentifier=mcore\n ,desc=Created by mm_createapplication\n ,params= param1=1&#x0a;param2=blah\n ,version=\n ,frefin=mm_in\n ,frefout=mm_out\n ,mDebug=1\n );`
expect(strictMacroDefinition.test(content2)).toEqual([])
})
it('should return an array with a single diagnostic when Macro definition has space in param', () => {
const content = `%macro
somemacro(va r1);`
expect(strictMacroDefinition.test(content)).toEqual([
{
message: `Param 'va r1' cannot have space`,
lineNumber: 2,
startColumnNumber: 18,
endColumnNumber: 22,
severity: Severity.Warning
}
])
})
it('should return an array with a two diagnostics when Macro definition has space in params', () => {
const content = `%macro somemacro(
var1,
var 2,
v ar3,
var4);`
expect(strictMacroDefinition.test(content)).toEqual([
{
message: `Param 'var 2' cannot have space`,
lineNumber: 3,
startColumnNumber: 7,
endColumnNumber: 11,
severity: Severity.Warning
},
{
message: `Param 'v ar3' cannot have space`,
lineNumber: 4,
startColumnNumber: 7,
endColumnNumber: 11,
severity: Severity.Warning
}
])
})
it('should return an array with a two diagnostics when Macro definition has space in params - special case', () => {
const content = `%macro macroName(
arr,
ar r/* / store source */ 3
) /* / store source */;/* / store source */`
expect(strictMacroDefinition.test(content)).toEqual([
{
message: `Param 'ar r 3' cannot have space`,
lineNumber: 3,
startColumnNumber: 7,
endColumnNumber: 32,
severity: Severity.Warning
}
])
})
it('should return an array with a single diagnostic when Macro definition has invalid option', () => {
const content = `%macro somemacro(var1, var2)
/minXoperator;`
expect(strictMacroDefinition.test(content)).toEqual([
{
message: `Option 'minXoperator' is not valid`,
lineNumber: 2,
startColumnNumber: 8,
endColumnNumber: 19,
severity: Severity.Warning
}
])
})
it('should return an array with a two diagnostics when Macro definition has invalid options', () => {
const content = `%macro
somemacro(
var1, var2
)
/ store
invalidoption
secure ;`
expect(strictMacroDefinition.test(content)).toEqual([
{
message: `Option 'invalidoption' is not valid`,
lineNumber: 6,
startColumnNumber: 16,
endColumnNumber: 28,
severity: Severity.Warning
}
])
})
})
})

View File

@@ -0,0 +1,169 @@
import { Diagnostic, LintConfig, Macro, Severity } from '../../types'
import { FileLintRule } from '../../types/LintRule'
import { LintRuleType } from '../../types/LintRuleType'
import { parseMacros } from '../../utils/parseMacros'
const name = 'strictMacroDefinition'
const description = 'Enforce strictly rules of macro definition syntax.'
const message = 'Incorrent Macro Definition Syntax'
const validOptions = [
'CMD',
'DES',
'MINDELIMITER',
'MINOPERATOR',
'NOMINOPERATOR',
'PARMBUFF',
'SECURE',
'NOSECURE',
'STMT',
'SOURCE',
'SRC',
'STORE'
]
const processParams = (
content: string,
macro: Macro,
diagnostics: Diagnostic[]
): string => {
const declaration = macro.declaration
const regExpParams = new RegExp(/(?<=\().*(?=\))/)
const regExpParamsResult = regExpParams.exec(declaration)
let _declaration = declaration
if (regExpParamsResult) {
const paramsPresent = regExpParamsResult[0]
const params = paramsPresent.trim().split(',')
params.forEach((param) => {
const trimedParam = param.split('=')[0].trim()
let paramLineNumber: number = 1,
paramStartIndex: number = 1,
paramEndIndex: number = content.length
if (
macro.declarationLines.findIndex(
(dl) => dl.indexOf(trimedParam) !== -1
) === -1
) {
const comment = '/\\*(.*?)\\*/'
for (let i = 1; i < trimedParam.length; i++) {
const paramWithComment =
trimedParam.slice(0, i) + comment + trimedParam.slice(i)
const regEx = new RegExp(paramWithComment)
const declarationLineIndex = macro.declarationLines.findIndex(
(dl) => !!regEx.exec(dl)
)
if (declarationLineIndex !== -1) {
const declarationLine = macro.declarationLines[declarationLineIndex]
const partFound = regEx.exec(declarationLine)![0]
paramLineNumber = macro.startLineNumbers[declarationLineIndex]
paramStartIndex = declarationLine.indexOf(partFound)
paramEndIndex =
declarationLine.indexOf(partFound) + partFound.length
break
}
}
} else {
const declarationLineIndex = macro.declarationLines.findIndex(
(dl) => dl.indexOf(trimedParam) !== -1
)
const declarationLine = macro.declarationLines[declarationLineIndex]
paramLineNumber = macro.startLineNumbers[declarationLineIndex]
paramStartIndex = declarationLine.indexOf(trimedParam)
paramEndIndex =
declarationLine.indexOf(trimedParam) + trimedParam.length
}
if (trimedParam.includes(' ')) {
diagnostics.push({
message: `Param '${trimedParam}' cannot have space`,
lineNumber: paramLineNumber,
startColumnNumber: paramStartIndex + 1,
endColumnNumber: paramEndIndex,
severity: Severity.Warning
})
}
})
_declaration = declaration.split(`(${paramsPresent})`)[1]
}
return _declaration
}
const processOptions = (
_declaration: string,
macro: Macro,
diagnostics: Diagnostic[]
): void => {
let optionsPresent = _declaration.split('/')?.[1]?.trim()
if (optionsPresent) {
const regex = new RegExp(/="(.*?)"/, 'g')
let result = regex.exec(optionsPresent)
// removing Option's `="..."` part, e.g. des="..."
while (result) {
optionsPresent =
optionsPresent.slice(0, result.index) +
optionsPresent.slice(result.index + result[0].length)
result = regex.exec(optionsPresent)
}
optionsPresent
.split(' ')
?.filter((o) => !!o)
.forEach((option) => {
const trimmedOption = option.trim()
if (!validOptions.includes(trimmedOption.toUpperCase())) {
const declarationLineIndex = macro.declarationLines.findIndex(
(dl) => dl.indexOf(trimmedOption) !== -1
)
const declarationLine = macro.declarationLines[declarationLineIndex]
diagnostics.push({
message: `Option '${trimmedOption}' is not valid`,
lineNumber: macro.startLineNumbers[declarationLineIndex],
startColumnNumber: declarationLine.indexOf(trimmedOption) + 1,
endColumnNumber:
declarationLine.indexOf(trimmedOption) + trimmedOption.length,
severity: Severity.Warning
})
}
})
}
}
const test = (value: string, config?: LintConfig) => {
const diagnostics: Diagnostic[] = []
const macros = parseMacros(value, config)
macros.forEach((macro) => {
const _declaration = processParams(value, macro, diagnostics)
processOptions(_declaration, macro, diagnostics)
})
return diagnostics
}
/**
* Lint rule that checks if a line has followed syntax for macro definition
*/
export const strictMacroDefinition: FileLintRule = {
type: LintRuleType.File,
name,
description,
message,
test
}

View File

@@ -3,4 +3,3 @@ export { maxLineLength } from './maxLineLength'
export { noEncodedPasswords } from './noEncodedPasswords'
export { noTabIndentation } from './noTabIndentation'
export { noTrailingSpaces } from './noTrailingSpaces'
export { strictMacroDefinition } from './strictMacroDefinition'

View File

@@ -1,88 +0,0 @@
import { LintConfig, Severity } from '../../types'
import { strictMacroDefinition } from './strictMacroDefinition'
describe('strictMacroDefinition', () => {
it('should return an empty array when the line has correct macro definition syntax', () => {
const line = '%macro somemacro;'
expect(strictMacroDefinition.test(line, 1)).toEqual([])
const line2 = '%macro somemacro();'
expect(strictMacroDefinition.test(line2, 1)).toEqual([])
const line3 = '%macro somemacro(var1);'
expect(strictMacroDefinition.test(line3, 1)).toEqual([])
const line4 = '%macro somemacro/minoperator;'
expect(strictMacroDefinition.test(line4, 1)).toEqual([])
const line5 = '%macro somemacro /minoperator;'
expect(strictMacroDefinition.test(line5, 1)).toEqual([])
const line6 = '%macro somemacro(var1, var2)/minoperator;'
expect(strictMacroDefinition.test(line6, 1)).toEqual([])
const line7 =
' /* Some Comment */ %macro somemacro(var1, var2) /minoperator ; /* Some Comment */'
expect(strictMacroDefinition.test(line7, 1)).toEqual([])
})
it('should return an array with a single diagnostic when Macro definition has space in param', () => {
const line = '%macro somemacro(va r1);'
expect(strictMacroDefinition.test(line, 1)).toEqual([
{
message: `Param 'va r1' cannot have space`,
lineNumber: 1,
startColumnNumber: 18,
endColumnNumber: 22,
severity: Severity.Warning
}
])
})
it('should return an array with a two diagnostics when Macro definition has space in param', () => {
const line = '%macro somemacro(var1, var 2, v ar3, var4);'
expect(strictMacroDefinition.test(line, 1)).toEqual([
{
message: `Param 'var 2' cannot have space`,
lineNumber: 1,
startColumnNumber: 24,
endColumnNumber: 28,
severity: Severity.Warning
},
{
message: `Param 'v ar3' cannot have space`,
lineNumber: 1,
startColumnNumber: 31,
endColumnNumber: 35,
severity: Severity.Warning
}
])
})
it('should return an array with a single diagnostic when Macro definition has invalid option', () => {
const line = '%macro somemacro(var1, var2)/minXoperator;'
expect(strictMacroDefinition.test(line, 1)).toEqual([
{
message: `Option 'minXoperator' is not valid`,
lineNumber: 1,
startColumnNumber: 30,
endColumnNumber: 41,
severity: Severity.Warning
}
])
})
it('should return an array with a two diagnostics when Macro definition has invalid options', () => {
const line =
'%macro somemacro(var1, var2)/ store invalidoption secure ;'
expect(strictMacroDefinition.test(line, 1)).toEqual([
{
message: `Option 'invalidoption' is not valid`,
lineNumber: 1,
startColumnNumber: 39,
endColumnNumber: 51,
severity: Severity.Warning
}
])
})
})

View File

@@ -1,88 +0,0 @@
import { Diagnostic } from '../../types/Diagnostic'
import { LintConfig } from '../../types'
import { LineLintRule } from '../../types/LintRule'
import { LintRuleType } from '../../types/LintRuleType'
import { Severity } from '../../types/Severity'
import { parseMacros } from '../../utils/parseMacros'
const name = 'strictMacroDefinition'
const description = 'Enforce strictly rules of macro definition syntax.'
const message = 'Incorrent Macro Definition Syntax'
const validOptions = [
'CMD',
'DES',
'MINDELIMITER',
'MINOPERATOR',
'NOMINOPERATOR',
'PARMBUFF',
'SECURE',
'NOSECURE',
'STMT',
'SOURCE',
'SRC',
'STORE'
]
const test = (value: string, lineNumber: number) => {
const diagnostics: Diagnostic[] = []
const macros = parseMacros(value)
const declaration = macros[0]?.declaration
if (!declaration) return []
const regExpParams = new RegExp(/\((.*?)\)/)
const regExpParamsResult = regExpParams.exec(declaration)
let _declaration = declaration
if (regExpParamsResult) {
const paramsPresent = regExpParamsResult[1]
const paramsTrimmed = paramsPresent.trim()
const params = paramsTrimmed.split(',')
params.forEach((param) => {
const trimedParam = param.split('=')[0].trim()
if (trimedParam.includes(' ')) {
diagnostics.push({
message: `Param '${trimedParam}' cannot have space`,
lineNumber,
startColumnNumber: value.indexOf(trimedParam) + 1,
endColumnNumber: value.indexOf(trimedParam) + trimedParam.length,
severity: Severity.Warning
})
}
})
_declaration = declaration.split(`(${paramsPresent})`)[1]
}
const optionsPresent = _declaration.split('/')?.[1]?.trim().split(' ')
optionsPresent
?.filter((o) => !!o)
.forEach((option) => {
const trimmedOption = option.trim()
if (!validOptions.includes(trimmedOption.toUpperCase())) {
diagnostics.push({
message: `Option '${trimmedOption}' is not valid`,
lineNumber,
startColumnNumber: value.indexOf(trimmedOption) + 1,
endColumnNumber: value.indexOf(trimmedOption) + trimmedOption.length,
severity: Severity.Warning
})
}
})
return diagnostics
}
/**
* Lint rule that checks if a line has followed syntax for macro definition
*/
export const strictMacroDefinition: LineLintRule = {
type: LintRuleType.Line,
name,
description,
message,
test
}

View File

@@ -3,15 +3,15 @@ import {
hasMacroNameInMend,
noNestedMacros,
hasMacroParentheses,
lineEndings
lineEndings,
strictMacroDefinition
} from '../rules/file'
import {
indentationMultiple,
maxLineLength,
noEncodedPasswords,
noTabIndentation,
noTrailingSpaces,
strictMacroDefinition
noTrailingSpaces
} from '../rules/line'
import { lowerCaseFileNames, noSpacesInFileNames } from '../rules/path'
import { LineEndings } from './LineEndings'
@@ -93,7 +93,7 @@ export class LintConfig {
}
if (json?.strictMacroDefinition) {
this.lineLintRules.push(strictMacroDefinition)
this.fileLintRules.push(strictMacroDefinition)
}
}
}

12
src/types/Macro.ts Normal file
View File

@@ -0,0 +1,12 @@
export interface Macro {
name: string
startLineNumbers: number[]
endLineNumber: number | null
declarationLines: string[]
terminationLine: string
declaration: string
termination: string
parentMacro: string
hasMacroNameInMend: boolean
mismatchedMendMacroName: string
}

View File

@@ -4,3 +4,4 @@ export * from './LintConfig'
export * from './LintRule'
export * from './LintRuleType'
export * from './Severity'
export * from './Macro'

View File

@@ -2,8 +2,8 @@ import * as fileModule from '@sasjs/utils/file'
import { LintConfig } from '../types/LintConfig'
import { getLintConfig } from './getLintConfig'
const expectedFileLintRulesCount = 4
const expectedLineLintRulesCount = 6
const expectedFileLintRulesCount = 5
const expectedLineLintRulesCount = 5
const expectedPathLintRulesCount = 2
describe('getLintConfig', () => {

View File

@@ -3,173 +3,277 @@ import { parseMacros } from './parseMacros'
describe('parseMacros', () => {
it('should return an array with a single macro', () => {
const text = `%macro test;
%put 'hello';
%mend`
const text = ` %macro test;\n %put 'hello';\n%mend`
const macros = parseMacros(text, new LintConfig())
expect(macros.length).toEqual(1)
expect(macros).toContainEqual({
name: 'test',
declarationLine: '%macro test;',
declarationLines: [' %macro test;'],
terminationLine: '%mend',
declaration: '%macro test',
termination: '%mend',
startLineNumber: 1,
startLineNumbers: [1],
endLineNumber: 3,
parentMacro: '',
hasMacroNameInMend: false,
hasParentheses: false,
mismatchedMendMacroName: ''
})
})
it('should return an array with a single macro having parameters', () => {
const text = `%macro test(var,sum);
%put 'hello';
%mend`
const text = `%macro test(var,sum);\n %put 'hello';\n%mend`
const macros = parseMacros(text, new LintConfig())
expect(macros.length).toEqual(1)
expect(macros).toContainEqual({
name: 'test',
declarationLine: '%macro test(var,sum);',
declarationLines: ['%macro test(var,sum);'],
terminationLine: '%mend',
declaration: '%macro test(var,sum)',
termination: '%mend',
startLineNumber: 1,
startLineNumbers: [1],
endLineNumber: 3,
parentMacro: '',
hasMacroNameInMend: false,
hasParentheses: false,
mismatchedMendMacroName: ''
})
})
it('should return an array with a single macro having PARMBUFF option', () => {
const text = `%macro test/parmbuff;
%put 'hello';
%mend`
const text = `%macro test/parmbuff;\n %put 'hello';\n%mend`
const macros = parseMacros(text, new LintConfig())
expect(macros.length).toEqual(1)
expect(macros).toContainEqual({
name: 'test',
declarationLine: '%macro test/parmbuff;',
declarationLines: ['%macro test/parmbuff;'],
terminationLine: '%mend',
declaration: '%macro test/parmbuff',
termination: '%mend',
startLineNumber: 1,
startLineNumbers: [1],
endLineNumber: 3,
parentMacro: '',
hasMacroNameInMend: false,
hasParentheses: false,
mismatchedMendMacroName: ''
})
})
it('should return an array with a single macro having paramerter & SOURCE option', () => {
const text = `/* commentary */ %macro foobar(arg) /store source
des="This macro does not do much";
%put 'hello';
%mend`
const text = `/* commentary */ %macro foobar(arg) /store source\n des="This macro does not do much";\n %put 'hello';\n%mend`
const macros = parseMacros(text, new LintConfig())
expect(macros.length).toEqual(1)
expect(macros).toContainEqual({
name: 'foobar',
declarationLine: '/* commentary */ %macro foobar(arg) /store source',
declarationLines: [
'/* commentary */ %macro foobar(arg) /store source',
' des="This macro does not do much";'
],
terminationLine: '%mend',
declaration: '%macro foobar(arg) /store source',
declaration:
'%macro foobar(arg) /store source des="This macro does not do much"',
termination: '%mend',
startLineNumber: 1,
startLineNumbers: [1, 2],
endLineNumber: 4,
parentMacro: '',
hasMacroNameInMend: false,
hasParentheses: false,
mismatchedMendMacroName: ''
})
})
it('should return an array with multiple macros', () => {
const text = `%macro foo;
%put 'foo';
%mend;
%macro bar();
%put 'bar';
%mend bar;`
const text = `%macro foo;\n %put 'foo';\n%mend;\n%macro bar();\n %put 'bar';\n%mend bar;`
const macros = parseMacros(text, new LintConfig())
expect(macros.length).toEqual(2)
expect(macros).toContainEqual({
name: 'foo',
declarationLine: '%macro foo;',
declarationLines: ['%macro foo;'],
terminationLine: '%mend;',
declaration: '%macro foo',
termination: '%mend',
startLineNumber: 1,
startLineNumbers: [1],
endLineNumber: 3,
parentMacro: '',
hasMacroNameInMend: false,
hasParentheses: false,
mismatchedMendMacroName: ''
})
expect(macros).toContainEqual({
name: 'bar',
declarationLine: '%macro bar();',
declarationLines: ['%macro bar();'],
terminationLine: '%mend bar;',
declaration: '%macro bar()',
termination: '%mend bar',
startLineNumber: 4,
startLineNumbers: [4],
endLineNumber: 6,
parentMacro: '',
hasMacroNameInMend: true,
hasParentheses: true,
mismatchedMendMacroName: ''
})
})
it('should detect nested macro definitions', () => {
const text = `%macro test()
%put 'hello';
%macro test2
%put 'world;
%mend
%mend test`
const text = `%macro test();\n %put 'hello';\n %macro test2;\n %put 'world;\n %mend\n%mend test`
const macros = parseMacros(text, new LintConfig())
expect(macros.length).toEqual(2)
expect(macros).toContainEqual({
name: 'test',
declarationLine: '%macro test()',
declarationLines: ['%macro test();'],
terminationLine: '%mend test',
declaration: '%macro test()',
termination: '%mend test',
startLineNumber: 1,
startLineNumbers: [1],
endLineNumber: 6,
parentMacro: '',
hasMacroNameInMend: true,
hasParentheses: true,
mismatchedMendMacroName: ''
})
expect(macros).toContainEqual({
name: 'test2',
declarationLine: ' %macro test2',
declarationLines: [' %macro test2;'],
terminationLine: ' %mend',
declaration: '%macro test2',
termination: '%mend',
startLineNumber: 3,
startLineNumbers: [3],
endLineNumber: 5,
parentMacro: 'test',
hasMacroNameInMend: false,
hasParentheses: false,
mismatchedMendMacroName: ''
})
})
describe(`multi-line macro declarations`, () => {
it('should return an array with a single macro', () => {
const text = `%macro \n test;\n %put 'hello';\n%mend`
const macros = parseMacros(text, new LintConfig())
expect(macros.length).toEqual(1)
expect(macros).toContainEqual({
name: 'test',
declarationLines: ['%macro ', ' test;'],
terminationLine: '%mend',
declaration: '%macro test',
termination: '%mend',
startLineNumbers: [1, 2],
endLineNumber: 4,
parentMacro: '',
hasMacroNameInMend: false,
mismatchedMendMacroName: ''
})
})
it('should return an array with a single macro having parameters', () => {
const text = `%macro \n test(\n var,\n sum);%put 'hello';\n%mend`
const macros = parseMacros(text, new LintConfig())
expect(macros.length).toEqual(1)
expect(macros).toContainEqual({
name: 'test',
declarationLines: [
'%macro ',
` test(`,
` var,`,
` sum);%put 'hello';`
],
terminationLine: '%mend',
declaration: '%macro test( var, sum)',
termination: '%mend',
startLineNumbers: [1, 2, 3, 4],
endLineNumber: 5,
parentMacro: '',
hasMacroNameInMend: false,
mismatchedMendMacroName: ''
})
})
it('should return an array with a single macro having PARMBUFF option', () => {
const text = `%macro test\n /parmbuff;\n %put 'hello';\n%mend`
const macros = parseMacros(text, new LintConfig())
expect(macros.length).toEqual(1)
expect(macros).toContainEqual({
name: 'test',
declarationLines: ['%macro test', ' /parmbuff;'],
terminationLine: '%mend',
declaration: '%macro test /parmbuff',
termination: '%mend',
startLineNumbers: [1, 2],
endLineNumber: 4,
parentMacro: '',
hasMacroNameInMend: false,
mismatchedMendMacroName: ''
})
})
it('should return an array with a single macro having paramerter & SOURCE option', () => {
const text = `/* commentary */ %macro foobar/* commentary */(arg) \n /* commentary */\n /store\n /* commentary */source\n des="This macro does not do much";\n %put 'hello';\n%mend`
const macros = parseMacros(text, new LintConfig())
expect(macros.length).toEqual(1)
expect(macros).toContainEqual({
name: 'foobar',
declarationLines: [
'/* commentary */ %macro foobar/* commentary */(arg) ',
' /* commentary */',
' /store',
' /* commentary */source',
' des="This macro does not do much";'
],
terminationLine: '%mend',
declaration:
'%macro foobar(arg) /store source des="This macro does not do much"',
termination: '%mend',
startLineNumbers: [1, 2, 3, 4, 5],
endLineNumber: 7,
parentMacro: '',
hasMacroNameInMend: false,
mismatchedMendMacroName: ''
})
})
it('should return an array with a single macro having semi-colon in params', () => {
const text = `\n%macro mm_createapplication(\n tree=/User Folders/sasdemo\n ,name=myApp\n ,ClassIdentifier=mcore\n ,desc=Created by mm_createapplication\n ,params= param1=1&#x0a;param2=blah\n ,version=\n ,frefin=mm_in\n ,frefout=mm_out\n ,mDebug=1\n );`
const macros = parseMacros(text, new LintConfig())
expect(macros.length).toEqual(1)
expect(macros).toContainEqual({
name: 'mm_createapplication',
declarationLines: [
`%macro mm_createapplication(`,
` tree=/User Folders/sasdemo`,
` ,name=myApp`,
` ,ClassIdentifier=mcore`,
` ,desc=Created by mm_createapplication`,
` ,params= param1=1&#x0a;param2=blah`,
` ,version=`,
` ,frefin=mm_in`,
` ,frefout=mm_out`,
` ,mDebug=1`,
` );`
],
terminationLine: '',
declaration:
'%macro mm_createapplication( tree=/User Folders/sasdemo ,name=myApp ,ClassIdentifier=mcore ,desc=Created by mm_createapplication ,params= param1=1&#x0a;param2=blah ,version= ,frefin=mm_in ,frefout=mm_out ,mDebug=1 )',
termination: '',
startLineNumbers: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
endLineNumber: null,
parentMacro: '',
hasMacroNameInMend: false,
mismatchedMendMacroName: ''
})
})
})
})

View File

@@ -1,21 +1,7 @@
import { LintConfig } from '../types/LintConfig'
import { LintConfig, Macro } from '../types'
import { LineEndings } from '../types/LineEndings'
import { trimComments } from './trimComments'
interface Macro {
name: string
startLineNumber: number | null
endLineNumber: number | null
declarationLine: string
terminationLine: string
declaration: string
termination: string
parentMacro: string
hasMacroNameInMend: boolean
hasParentheses: boolean
mismatchedMendMacroName: string
}
export const parseMacros = (text: string, config?: LintConfig): Macro[] => {
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
const lines: string[] = text ? text.split(lineEnding) : []
@@ -23,39 +9,107 @@ export const parseMacros = (text: string, config?: LintConfig): Macro[] => {
let isCommentStarted = false
let macroStack: Macro[] = []
lines.forEach((line, index) => {
let isReadingMacroDefinition = false
let isStatementContinues = true
let tempMacroDeclaration = ''
let tempMacroDeclarationLines: string[] = []
let tempStartLineNumbers: number[] = []
lines.forEach((line, lineIndex) => {
const { statement: trimmedLine, commentStarted } = trimComments(
line,
isCommentStarted
)
isCommentStarted = commentStarted
const statements: string[] = trimmedLine ? trimmedLine.split(';') : []
statements.forEach((statement) => {
isStatementContinues = !trimmedLine.endsWith(';')
const statements: string[] = trimmedLine.split(';')
if (isReadingMacroDefinition) {
// checking if code is split into statements based on `;` is a part of HTML Encoded Character
// if it happened, merges two statements into one
statements.forEach((statement, statementIndex) => {
if (/&[^\s]{1,5}$/.test(statement)) {
const next = statements[statementIndex]
const updatedStatement = `${statement};${
statements[statementIndex + 1]
}`
statements.splice(statementIndex, 1, updatedStatement)
statements.splice(statementIndex + 1, 1)
}
})
}
statements.forEach((statement, statementIndex) => {
const { statement: trimmedStatement, commentStarted } = trimComments(
statement,
isCommentStarted
)
isCommentStarted = commentStarted
if (isReadingMacroDefinition) {
tempMacroDeclaration =
tempMacroDeclaration +
(trimmedStatement ? ' ' + trimmedStatement : '')
tempMacroDeclarationLines.push(line)
tempStartLineNumbers.push(lineIndex + 1)
if (!Object.is(statements.length - 1, statementIndex)) {
isReadingMacroDefinition = false
const name = tempMacroDeclaration
.slice(7, tempMacroDeclaration.length)
.trim()
.split('/')[0]
.split('(')[0]
.trim()
macroStack.push({
name,
startLineNumbers: tempStartLineNumbers,
endLineNumber: null,
parentMacro: macroStack.length
? macroStack[macroStack.length - 1].name
: '',
hasMacroNameInMend: false,
mismatchedMendMacroName: '',
declarationLines: tempMacroDeclarationLines,
terminationLine: '',
declaration: tempMacroDeclaration,
termination: ''
})
}
}
if (trimmedStatement.startsWith('%macro')) {
const startLineNumber = index + 1
const startLineNumber = lineIndex + 1
if (
isStatementContinues &&
Object.is(statements.length - 1, statementIndex)
) {
tempMacroDeclaration = trimmedStatement
tempMacroDeclarationLines = [line]
tempStartLineNumbers = [startLineNumber]
isReadingMacroDefinition = true
return
}
const name = trimmedStatement
.slice(7, trimmedStatement.length)
.trim()
.split('/')[0]
.split('(')[0]
.trim()
macroStack.push({
name,
startLineNumber,
startLineNumbers: [startLineNumber],
endLineNumber: null,
parentMacro: macroStack.length
? macroStack[macroStack.length - 1].name
: '',
hasParentheses: trimmedStatement.endsWith('()'),
hasMacroNameInMend: false,
mismatchedMendMacroName: '',
declarationLine: line,
declarationLines: [line],
terminationLine: '',
declaration: trimmedStatement,
termination: ''
@@ -65,7 +119,7 @@ export const parseMacros = (text: string, config?: LintConfig): Macro[] => {
const macro = macroStack.pop() as Macro
const mendMacroName =
trimmedStatement.split(' ').filter((s: string) => !!s)[1] || ''
macro.endLineNumber = index + 1
macro.endLineNumber = lineIndex + 1
macro.hasMacroNameInMend = mendMacroName === macro.name
macro.mismatchedMendMacroName = macro.hasMacroNameInMend
? ''
@@ -76,13 +130,12 @@ export const parseMacros = (text: string, config?: LintConfig): Macro[] => {
} else {
macros.push({
name: '',
startLineNumber: null,
endLineNumber: index + 1,
startLineNumbers: [],
endLineNumber: lineIndex + 1,
parentMacro: '',
hasParentheses: false,
hasMacroNameInMend: false,
mismatchedMendMacroName: '',
declarationLine: '',
declarationLines: [],
terminationLine: line,
declaration: '',
termination: trimmedStatement

View File

@@ -7,6 +7,27 @@ describe('trimComments', () => {
/* some comment */ some code;
`)
).toEqual({ statement: 'some code;', commentStarted: false })
expect(
trimComments(`
/*/ some comment */ some code;
`)
).toEqual({ statement: 'some code;', commentStarted: false })
expect(
trimComments(`
some code;/*/ some comment */ some code;
`)
).toEqual({ statement: 'some code; some code;', commentStarted: false })
expect(
trimComments(`/* some comment */
/* some comment */ CODE_Keyword1 /* some comment */ CODE_Keyword2/* some comment */;/* some comment */
/* some comment */`)
).toEqual({
statement: 'CODE_Keyword1 CODE_Keyword2;',
commentStarted: false
})
})
it('should return statment, having multi-line comment', () => {

View File

@@ -1,11 +1,14 @@
export const trimComments = (
statement: string,
commentStarted: boolean = false
commentStarted: boolean = false,
trimEnd: boolean = false
): { statement: string; commentStarted: boolean } => {
let trimmed = (statement || '').trim()
let trimmed = trimEnd ? (statement || '').trimEnd() : (statement || '').trim()
if (commentStarted || trimmed.startsWith('/*')) {
const parts = trimmed.split('*/')
const parts = trimmed.startsWith('/*')
? trimmed.slice(2).split('*/')
: trimmed.split('*/')
if (parts.length === 2) {
return {
statement: (parts.pop() as string).trim(),
@@ -17,6 +20,19 @@ export const trimComments = (
} else {
return { statement: '', commentStarted: true }
}
} else if (trimmed.includes('/*')) {
const statementBeforeCommentStarts = trimmed.slice(0, trimmed.indexOf('/*'))
trimmed = trimmed.slice(trimmed.indexOf('/*') + 2)
const remainingStatement = trimmed.slice(trimmed.indexOf('*/') + 2)
const result = trimComments(remainingStatement, false, true)
const completeStatement = statementBeforeCommentStarts + result.statement
return {
statement: trimEnd
? completeStatement.trimEnd()
: completeStatement.trim(),
commentStarted: result.commentStarted
}
}
return { statement: trimmed, commentStarted: false }
}