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

Compare commits

..

39 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
Krishna Acondy
482ecec150 Merge pull request #54 from sasjs/issue53
Issue53
2021-05-20 16:35:57 +01:00
Saad Jutt
b4ec32b72c chore: updated sasjslint-schema.json 2021-05-20 19:22:08 +05:00
Saad Jutt
dcfeb7a641 feat: new rule added 'strictMacroDefinition' 2021-05-20 19:14:01 +05:00
Saad Jutt
e5780cd69a fix(parseMacro): updated to have macros with parameters + options 2021-05-20 15:12:06 +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
Yury Shkoda
5a358330c0 Merge pull request #50 from sasjs/git-hooks
feat(git): enabled git hook enforcing conventional commits
2021-05-20 09:01:47 +03:00
Yury Shkoda
fa9e4136bc chore(lint): fixed linting 2021-05-20 08:58:25 +03:00
Yury Shkoda
0c9b23c51b feat(git): enabled git hook enforcing conventional commits 2021-05-20 08:54:36 +03:00
Allan Bowe
9daf8f8c82 Update README.md 2021-05-06 11:02:54 +03:00
Allan Bowe
7ed846e3aa setting macronameinmend to true in readme 2021-05-06 11:02:29 +03:00
26 changed files with 966 additions and 153 deletions

18
.git-hooks/commit-msg Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/sh
RED="\033[1;31m"
GREEN="\033[1;32m"
# Get the commit message (the parameter we're given is just the path to the
# temporary file which holds the message).
commit_message=$(cat "$1")
if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z \-]+\))?!?: .+$") then
echo "${GREEN} ✔ Commit message meets Conventional Commit standards"
exit 0
fi
echo "${RED}❌ Commit message does not meet the Conventional Commit standard!"
echo "An example of a valid message is:"
echo " feat(login): add the 'remember me' button"
echo " More details at: https://www.conventionalcommits.org/en/v1.0.0/#summary"
exit 1

View File

@@ -16,7 +16,7 @@ Configuration is via a `.sasjslint` file with the following structure (these are
{
"noEncodedPasswords": true,
"hasDoxygenHeader": true,
"hasMacroNameInMend": false,
"hasMacroNameInMend": true,
"hasMacroParentheses": true,
"indentationMultiple": 2,
"lowerCaseFileNames": true,
@@ -44,9 +44,9 @@ 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: false (for now)
* Default: true
* Severity: WARNING
#### hasMacroParentheses
@@ -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

@@ -8,7 +8,8 @@
"postpublish": "git clean -fd",
"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}'"
"lint": "npx prettier --check '{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
"prepare": "git rev-parse --git-dir && git config core.hooksPath ./.git-hooks || true"
},
"publishConfig": {
"access": "public"
@@ -37,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

@@ -16,7 +16,8 @@
"noSpacesInFileNames": true,
"noTabIndentation": true,
"noTrailingSpaces": true,
"lineEndings": "lf"
"lineEndings": "lf",
"strictMacroDefinition": true
},
"examples": [
{
@@ -31,7 +32,8 @@
"hasMacroNameInMend": true,
"noNestedMacros": true,
"hasMacroParentheses": true,
"lineEndings": "crlf"
"lineEndings": "crlf",
"strictMacroDefinition": true
}
],
"properties": {
@@ -130,6 +132,14 @@
"description": "Enforces the configured terminating character for each line. Shows a warning when incorrect line endings are present.",
"default": "lf",
"examples": ["lf", "crlf"]
},
"strictMacroDefinition": {
"$id": "#/properties/strictMacroDefinition",
"type": "boolean",
"title": "strictMacroDefinition",
"description": "Enforces Macro Definition syntax. Shows a warning when incorrect syntax is used.",
"default": true,
"examples": [true, false]
}
}
}

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);
@@ -59,7 +85,7 @@ describe('hasDoxygenHeader - test', () => {
it('should return an array with a single diagnostic when the file is undefined', () => {
const content = undefined
expect(hasDoxygenHeader.test((content as unknown) as string)).toEqual([
expect(hasDoxygenHeader.test(content as unknown as string)).toEqual([
{
message: 'File missing Doxygen header',
lineNumber: 1,
@@ -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([
{
@@ -114,7 +114,7 @@ describe('hasMacroNameInMend - test', () => {
it('should return an empty array when the file is undefined', () => {
const content = undefined
expect(hasMacroNameInMend.test((content as unknown) as string)).toEqual([])
expect(hasMacroNameInMend.test(content as unknown as string)).toEqual([])
})
describe('nestedMacros', () => {

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

@@ -79,7 +79,7 @@ describe('hasMacroParentheses', () => {
it('should return an empty array when the file is undefined', () => {
const content = undefined
expect(hasMacroParentheses.test((content as unknown) as string)).toEqual([])
expect(hasMacroParentheses.test(content as unknown as string)).toEqual([])
})
describe('with extra spaces and comments', () => {
@@ -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

@@ -72,7 +72,7 @@ describe('noNestedMacros', () => {
it('should return an empty array when the file is undefined', () => {
const content = undefined
expect(noNestedMacros.test((content as unknown) as string)).toEqual([])
expect(noNestedMacros.test(content as unknown as string)).toEqual([])
})
it('should use the configured line ending while testing content', () => {

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,7 +3,8 @@ import {
hasMacroNameInMend,
noNestedMacros,
hasMacroParentheses,
lineEndings
lineEndings,
strictMacroDefinition
} from '../rules/file'
import {
indentationMultiple,
@@ -90,5 +91,9 @@ export class LintConfig {
if (json?.hasMacroParentheses) {
this.fileLintRules.push(hasMacroParentheses)
}
if (json?.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,7 +2,7 @@ import * as fileModule from '@sasjs/utils/file'
import { LintConfig } from '../types/LintConfig'
import { getLintConfig } from './getLintConfig'
const expectedFileLintRulesCount = 4
const expectedFileLintRulesCount = 5
const expectedLineLintRulesCount = 5
const expectedPathLintRulesCount = 2

View File

@@ -17,7 +17,8 @@ export const DefaultLintConfiguration = {
indentationMultiple: 2,
hasMacroNameInMend: true,
noNestedMacros: true,
hasMacroParentheses: true
hasMacroParentheses: true,
strictMacroDefinition: true
}
/**

View File

@@ -3,103 +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);\n %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);'],
terminationLine: '%mend',
declaration: '%macro test(var,sum)',
termination: '%mend',
startLineNumbers: [1],
endLineNumber: 3,
parentMacro: '',
hasMacroNameInMend: false,
mismatchedMendMacroName: ''
})
})
it('should return an array with a single macro having PARMBUFF option', () => {
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',
declarationLines: ['%macro test/parmbuff;'],
terminationLine: '%mend',
declaration: '%macro test/parmbuff',
termination: '%mend',
startLineNumbers: [1],
endLineNumber: 3,
parentMacro: '',
hasMacroNameInMend: false,
mismatchedMendMacroName: ''
})
})
it('should return an array with a single macro having paramerter & SOURCE option', () => {
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',
declarationLines: [
'/* commentary */ %macro foobar(arg) /store 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],
endLineNumber: 4,
parentMacro: '',
hasMacroNameInMend: 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,38 +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: ''
@@ -64,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
? ''
@@ -75,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,19 +1,38 @@
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('*/')
if (parts.length > 1) {
const parts = trimmed.startsWith('/*')
? trimmed.slice(2).split('*/')
: trimmed.split('*/')
if (parts.length === 2) {
return {
statement: (parts.pop() as string).trim(),
commentStarted: false
}
} else if (parts.length > 2) {
parts.shift()
return trimComments(parts.join('*/'), false)
} 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 }
}