mirror of
https://github.com/sasjs/lint.git
synced 2025-12-10 17:34:36 +00:00
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b13302a315 | ||
|
|
0790a447f3 | ||
|
|
11182aaaa7 | ||
|
|
7144d0cfe3 | ||
|
|
0caf31b7ff | ||
|
|
020a1e08d0 | ||
|
|
a762dadf37 | ||
|
|
c9fa366130 | ||
|
|
5701064c07 | ||
|
|
cbfa1f40d1 | ||
|
|
d391a4e8fc | ||
|
|
f793eb3a76 | ||
|
|
af2d2c12c1 | ||
|
|
8bfb547427 | ||
|
|
d7721f8e5e | ||
|
|
482ecec150 | ||
|
|
b4ec32b72c | ||
|
|
dcfeb7a641 | ||
|
|
e5780cd69a | ||
|
|
021f36663a | ||
|
|
5a358330c0 | ||
|
|
fa9e4136bc | ||
|
|
0c9b23c51b | ||
|
|
9daf8f8c82 | ||
|
|
7ed846e3aa | ||
|
|
f7f989fabd | ||
|
|
850cf85ef1 | ||
|
|
3dc304fffc | ||
|
|
e329529484 | ||
|
|
15190bfe88 | ||
|
|
bc011c4b47 | ||
|
|
a95c083b61 | ||
|
|
96fb384ec9 | ||
|
|
21fd4e8fcc | ||
|
|
ac595c65d0 | ||
|
|
e5763ce529 | ||
|
|
4729f04589 | ||
|
|
596d56c906 | ||
|
|
32956db8b2 | ||
|
|
7b58c455dc | ||
|
|
c86fd7dd1d | ||
|
|
34e9a7b139 | ||
|
|
5de3d33c1c | ||
|
|
3a6a5d30e3 | ||
|
|
0f629c4aca | ||
|
|
ae4c5e8347 | ||
|
|
3c700a97fc | ||
|
|
d113ef4ddd | ||
|
|
dce9453680 | ||
|
|
e76abc2db2 | ||
|
|
1e70b9debc | ||
|
|
984915fe47 | ||
|
|
2687a8fa46 | ||
|
|
3da3e1e134 | ||
|
|
abc2f75dc0 | ||
|
|
060b838f21 | ||
|
|
cd90b0850a | ||
|
|
db2dbb1c69 | ||
|
|
59f7e71919 | ||
|
|
6fd941aa2d | ||
|
|
93124bec5b | ||
|
|
bcb50b9968 | ||
|
|
d28d32d441 | ||
|
|
519a0164b5 | ||
|
|
99813f04c0 | ||
|
|
eb5a1bbbcb | ||
|
|
0c22ade942 | ||
|
|
33a57c3163 | ||
|
|
c2209cbe0e | ||
|
|
fe974050f7 | ||
|
|
1402802f0a | ||
|
|
36b3a7f319 | ||
|
|
c56887d6e6 | ||
|
|
031a323839 | ||
|
|
c9b6c3af95 |
18
.git-hooks/commit-msg
Executable file
18
.git-hooks/commit-msg
Executable 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
|
||||||
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 10
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
"lowerCaseFileNames": true,
|
"lowerCaseFileNames": true,
|
||||||
"noTabIndentation": true,
|
"noTabIndentation": true,
|
||||||
"indentationMultiple": 2,
|
"indentationMultiple": 2,
|
||||||
"hasMacroNameInMend": false,
|
"hasMacroNameInMend": true,
|
||||||
"noNestedMacros": true,
|
"noNestedMacros": true,
|
||||||
"hasMacroParentheses": true
|
"hasMacroParentheses": true
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
[](https://dependabot.com)
|
||||||
|
|
||||||
# SAS Code linting and formatting
|
# SAS Code linting and formatting
|
||||||
|
|
||||||
Our goal is to help SAS developers everywhere spend less time on code reviews, bug fixing and arguing about standards - and more time delivering extraordinary business value.
|
Our goal is to help SAS developers everywhere spend less time on code reviews, bug fixing and arguing about standards - and more time delivering extraordinary business value.
|
||||||
@@ -14,7 +16,7 @@ Configuration is via a `.sasjslint` file with the following structure (these are
|
|||||||
{
|
{
|
||||||
"noEncodedPasswords": true,
|
"noEncodedPasswords": true,
|
||||||
"hasDoxygenHeader": true,
|
"hasDoxygenHeader": true,
|
||||||
"hasMacroNameInMend": false,
|
"hasMacroNameInMend": true,
|
||||||
"hasMacroParentheses": true,
|
"hasMacroParentheses": true,
|
||||||
"indentationMultiple": 2,
|
"indentationMultiple": 2,
|
||||||
"lowerCaseFileNames": true,
|
"lowerCaseFileNames": true,
|
||||||
@@ -44,7 +46,7 @@ The SASjs framework recommends the use of Doxygen headers for describing all typ
|
|||||||
#### hasMacroNameInMend
|
#### 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 will be the result of a popular vote by around 300 people.
|
||||||
|
|
||||||
* Default: false (for now)
|
* Default: true
|
||||||
* Severity: WARNING
|
* Severity: WARNING
|
||||||
|
|
||||||
#### hasMacroParentheses
|
#### hasMacroParentheses
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ module.exports = {
|
|||||||
statements: -10
|
statements: -10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
collectCoverageFrom: ['src/**/{!(index|example),}.ts']
|
collectCoverageFrom: ['src/**/{!(index|formatExample|lintExample),}.ts']
|
||||||
}
|
}
|
||||||
|
|||||||
93
package-lock.json
generated
93
package-lock.json
generated
@@ -648,14 +648,35 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@sasjs/utils": {
|
"@sasjs/utils": {
|
||||||
"version": "2.10.1",
|
"version": "2.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.12.0.tgz",
|
||||||
"integrity": "sha512-T54jx6NEMLu2+R/ux4qcb3dDJ7nFrKkPCkmPXEfZxPQBkbq4C0kmaZv6dC63RDH68wYhoXR2S5fION5fFh91iw==",
|
"integrity": "sha512-OnC/7R+nGI8tlSPCcI7fPyD7T97B+McnkXT0IuAYDNGbfwRPuseWq0I1h+kbAWThGT67H4hnp61N0qr8LkpHZQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/prompts": "^2.0.9",
|
"chalk": "^4.1.1",
|
||||||
|
"cli-table": "^0.3.6",
|
||||||
"consola": "^2.15.0",
|
"consola": "^2.15.0",
|
||||||
"prompts": "^2.4.0",
|
"prompts": "^2.4.1",
|
||||||
"valid-url": "^1.0.9"
|
"valid-url": "^1.0.9"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
|
||||||
|
"requires": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prompts": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ==",
|
||||||
|
"requires": {
|
||||||
|
"kleur": "^3.0.3",
|
||||||
|
"sisteransi": "^1.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@sinonjs/commons": {
|
"@sinonjs/commons": {
|
||||||
@@ -751,9 +772,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/jest": {
|
"@types/jest": {
|
||||||
"version": "26.0.21",
|
"version": "26.0.23",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.23.tgz",
|
||||||
"integrity": "sha512-ab9TyM/69yg7eew9eOwKMUmvIZAKEGZYlq/dhe5/0IMUd/QLJv5ldRMdddSn+u22N13FP3s5jYyktxuBwY0kDA==",
|
"integrity": "sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"jest-diff": "^26.0.0",
|
"jest-diff": "^26.0.0",
|
||||||
@@ -761,9 +782,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "14.14.35",
|
"version": "15.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz",
|
||||||
"integrity": "sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag=="
|
"integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/normalize-package-data": {
|
"@types/normalize-package-data": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
@@ -777,14 +799,6 @@
|
|||||||
"integrity": "sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA==",
|
"integrity": "sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/prompts": {
|
|
||||||
"version": "2.0.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.0.10.tgz",
|
|
||||||
"integrity": "sha512-W3PEl3l4vmxdgfY6LUG7ysh+mLJOTOFYmSpiLe6MCo1OdEm8b5s6ZJfuTQgEpYNwcMiiaRzJespPS5Py2tqLlQ==",
|
|
||||||
"requires": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@types/stack-utils": {
|
"@types/stack-utils": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
|
||||||
@@ -881,7 +895,6 @@
|
|||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"color-convert": "^2.0.1"
|
"color-convert": "^2.0.1"
|
||||||
}
|
}
|
||||||
@@ -1278,6 +1291,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"cli-table": {
|
||||||
|
"version": "0.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.6.tgz",
|
||||||
|
"integrity": "sha512-ZkNZbnZjKERTY5NwC2SeMeLeifSPq/pubeRoTpdr3WchLlnZg6hEgvHkK5zL7KNFdd9PmHN8lxrENUwI3cE8vQ==",
|
||||||
|
"requires": {
|
||||||
|
"colors": "1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"cliui": {
|
"cliui": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||||
@@ -1315,7 +1336,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"color-name": "~1.1.4"
|
"color-name": "~1.1.4"
|
||||||
}
|
}
|
||||||
@@ -1323,8 +1343,7 @@
|
|||||||
"color-name": {
|
"color-name": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"colorette": {
|
"colorette": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
@@ -1332,6 +1351,11 @@
|
|||||||
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
|
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"colors": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
|
||||||
|
"integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs="
|
||||||
|
},
|
||||||
"combined-stream": {
|
"combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
@@ -2036,8 +2060,7 @@
|
|||||||
"has-flag": {
|
"has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"has-value": {
|
"has-value": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -3587,6 +3610,7 @@
|
|||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz",
|
||||||
"integrity": "sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==",
|
"integrity": "sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"kleur": "^3.0.3",
|
"kleur": "^3.0.3",
|
||||||
"sisteransi": "^1.0.5"
|
"sisteransi": "^1.0.5"
|
||||||
@@ -4395,7 +4419,6 @@
|
|||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"has-flag": "^4.0.0"
|
"has-flag": "^4.0.0"
|
||||||
}
|
}
|
||||||
@@ -4517,9 +4540,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ts-jest": {
|
"ts-jest": {
|
||||||
"version": "26.5.4",
|
"version": "26.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.6.tgz",
|
||||||
"integrity": "sha512-I5Qsddo+VTm94SukBJ4cPimOoFZsYTeElR2xy6H2TOVs+NsvgYglW8KuQgKoApOKuaU/Ix/vrF9ebFZlb5D2Pg==",
|
"integrity": "sha512-rua+rCP8DxpA8b4DQD/6X2HQS8Zy/xzViVYfEs2OQu68tkCuKLV0Md8pmX55+W24uRIyAsf/BajRfxOs+R2MKA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"bs-logger": "0.x",
|
"bs-logger": "0.x",
|
||||||
@@ -4535,9 +4558,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "7.3.4",
|
"version": "7.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||||
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
|
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"lru-cache": "^6.0.0"
|
"lru-cache": "^6.0.0"
|
||||||
@@ -4597,9 +4620,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz",
|
||||||
"integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==",
|
"integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"union-value": {
|
"union-value": {
|
||||||
|
|||||||
13
package.json
13
package.json
@@ -8,7 +8,8 @@
|
|||||||
"postpublish": "git clean -fd",
|
"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",
|
"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: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}'",
|
||||||
|
"postinstall": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
@@ -36,14 +37,14 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/sasjs/lint#readme",
|
"homepage": "https://github.com/sasjs/lint#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^26.0.21",
|
"@types/jest": "^26.0.23",
|
||||||
"@types/node": "^14.14.35",
|
"@types/node": "^15.0.2",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"ts-jest": "^26.5.4",
|
"ts-jest": "^26.5.6",
|
||||||
"typescript": "^4.2.3"
|
"typescript": "^4.2.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/utils": "^2.10.1"
|
"@sasjs/utils": "^2.12.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@
|
|||||||
"noNestedMacros": true,
|
"noNestedMacros": true,
|
||||||
"noSpacesInFileNames": true,
|
"noSpacesInFileNames": true,
|
||||||
"noTabIndentation": true,
|
"noTabIndentation": true,
|
||||||
"noTrailingSpaces": true
|
"noTrailingSpaces": true,
|
||||||
|
"lineEndings": "lf",
|
||||||
|
"strictMacroDefinition": true
|
||||||
},
|
},
|
||||||
"examples": [
|
"examples": [
|
||||||
{
|
{
|
||||||
@@ -29,7 +31,9 @@
|
|||||||
"indentationMultiple": 4,
|
"indentationMultiple": 4,
|
||||||
"hasMacroNameInMend": true,
|
"hasMacroNameInMend": true,
|
||||||
"noNestedMacros": true,
|
"noNestedMacros": true,
|
||||||
"hasMacroParentheses": true
|
"hasMacroParentheses": true,
|
||||||
|
"lineEndings": "crlf",
|
||||||
|
"strictMacroDefinition": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -120,6 +124,22 @@
|
|||||||
"description": "Enforces no trailing spaces in lines of SAS code. Shows a warning when they are present.",
|
"description": "Enforces no trailing spaces in lines of SAS code. Shows a warning when they are present.",
|
||||||
"default": true,
|
"default": true,
|
||||||
"examples": [true, false]
|
"examples": [true, false]
|
||||||
|
},
|
||||||
|
"lineEndings": {
|
||||||
|
"$id": "#/properties/lineEndings",
|
||||||
|
"type": "string",
|
||||||
|
"title": "lineEndings",
|
||||||
|
"description": "Enforces the configured terminating character for each line. Shows a warning when incorrect line endings are present.",
|
||||||
|
"default": "lf",
|
||||||
|
"examples": ["lf", "crlf"]
|
||||||
|
},
|
||||||
|
"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]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export const format = (text: string) => {}
|
|
||||||
97
src/format/formatFile.spec.ts
Normal file
97
src/format/formatFile.spec.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { formatFile } from './formatFile'
|
||||||
|
import path from 'path'
|
||||||
|
import { createFile, deleteFile, readFile } from '@sasjs/utils/file'
|
||||||
|
import { LintConfig } from '../types'
|
||||||
|
|
||||||
|
describe('formatFile', () => {
|
||||||
|
it('should fix linting issues in a given file', async () => {
|
||||||
|
const content = `%macro somemacro(); \n%put 'hello';\n%mend;`
|
||||||
|
const expectedContent = `/**\n @file\n @brief <Your brief here>\n <h4> SAS Macros </h4>\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;`
|
||||||
|
await createFile(path.join(__dirname, 'format-file-test.sas'), content)
|
||||||
|
const expectedResult = {
|
||||||
|
updatedFilePaths: [path.join(__dirname, 'format-file-test.sas')],
|
||||||
|
fixedDiagnosticsCount: 3,
|
||||||
|
unfixedDiagnostics: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await formatFile(
|
||||||
|
path.join(__dirname, 'format-file-test.sas')
|
||||||
|
)
|
||||||
|
const formattedContent = await readFile(
|
||||||
|
path.join(__dirname, 'format-file-test.sas')
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedResult)
|
||||||
|
expect(formattedContent).toEqual(expectedContent)
|
||||||
|
|
||||||
|
await deleteFile(path.join(__dirname, 'format-file-test.sas'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use the provided config if available', async () => {
|
||||||
|
const content = `%macro somemacro(); \n%put 'hello';\n%mend;`
|
||||||
|
const expectedContent = `/**\r\n @file\r\n @brief <Your brief here>\r\n <h4> SAS Macros </h4>\r\n**/\r\n%macro somemacro();\r\n%put 'hello';\r\n%mend;`
|
||||||
|
const expectedResult = {
|
||||||
|
updatedFilePaths: [path.join(__dirname, 'format-file-config.sas')],
|
||||||
|
fixedDiagnosticsCount: 2,
|
||||||
|
unfixedDiagnostics: [
|
||||||
|
{
|
||||||
|
endColumnNumber: 7,
|
||||||
|
lineNumber: 8,
|
||||||
|
message: '%mend statement is missing macro name - somemacro',
|
||||||
|
severity: 1,
|
||||||
|
startColumnNumber: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
await createFile(path.join(__dirname, 'format-file-config.sas'), content)
|
||||||
|
|
||||||
|
const result = await formatFile(
|
||||||
|
path.join(__dirname, 'format-file-config.sas'),
|
||||||
|
new LintConfig({
|
||||||
|
lineEndings: 'crlf',
|
||||||
|
hasMacroNameInMend: false,
|
||||||
|
hasDoxygenHeader: true,
|
||||||
|
noTrailingSpaces: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const formattedContent = await readFile(
|
||||||
|
path.join(__dirname, 'format-file-config.sas')
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedResult)
|
||||||
|
expect(formattedContent).toEqual(expectedContent)
|
||||||
|
|
||||||
|
await deleteFile(path.join(__dirname, 'format-file-config.sas'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not update any files if there are no formatting violations', async () => {
|
||||||
|
const content = `/**\r\n @file\r\n @brief <Your brief here>\r\n <h4> SAS Macros </h4>\r\n**/\r\n%macro somemacro();\r\n%put 'hello';\r\n%mend somemacro;`
|
||||||
|
const expectedResult = {
|
||||||
|
updatedFilePaths: [],
|
||||||
|
fixedDiagnosticsCount: 0,
|
||||||
|
unfixedDiagnostics: []
|
||||||
|
}
|
||||||
|
await createFile(
|
||||||
|
path.join(__dirname, 'format-file-no-violations.sas'),
|
||||||
|
content
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await formatFile(
|
||||||
|
path.join(__dirname, 'format-file-no-violations.sas'),
|
||||||
|
new LintConfig({
|
||||||
|
lineEndings: 'crlf',
|
||||||
|
hasMacroNameInMend: true,
|
||||||
|
hasDoxygenHeader: true,
|
||||||
|
noTrailingSpaces: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const formattedContent = await readFile(
|
||||||
|
path.join(__dirname, 'format-file-no-violations.sas')
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedResult)
|
||||||
|
expect(formattedContent).toEqual(content)
|
||||||
|
|
||||||
|
await deleteFile(path.join(__dirname, 'format-file-no-violations.sas'))
|
||||||
|
})
|
||||||
|
})
|
||||||
45
src/format/formatFile.ts
Normal file
45
src/format/formatFile.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { createFile, readFile } from '@sasjs/utils/file'
|
||||||
|
import { lintFile } from '../lint'
|
||||||
|
import { FormatResult } from '../types'
|
||||||
|
import { LintConfig } from '../types/LintConfig'
|
||||||
|
import { getLintConfig } from '../utils/getLintConfig'
|
||||||
|
import { processText } from './shared'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies automatic formatting to the file at the given path.
|
||||||
|
* @param {string} filePath - the path to the file to be formatted.
|
||||||
|
* @param {LintConfig} configuration - an optional configuration. When not passed in, this is read from the .sasjslint file.
|
||||||
|
* @returns {Promise<FormatResult>} Resolves successfully when the file has been formatted.
|
||||||
|
*/
|
||||||
|
export const formatFile = async (
|
||||||
|
filePath: string,
|
||||||
|
configuration?: LintConfig
|
||||||
|
): Promise<FormatResult> => {
|
||||||
|
const config = configuration || (await getLintConfig())
|
||||||
|
const diagnosticsBeforeFormat = await lintFile(filePath)
|
||||||
|
const diagnosticsCountBeforeFormat = diagnosticsBeforeFormat.length
|
||||||
|
|
||||||
|
const text = await readFile(filePath)
|
||||||
|
|
||||||
|
const formattedText = processText(text, config)
|
||||||
|
|
||||||
|
await createFile(filePath, formattedText)
|
||||||
|
|
||||||
|
const diagnosticsAfterFormat = await lintFile(filePath)
|
||||||
|
const diagnosticsCountAfterFormat = diagnosticsAfterFormat.length
|
||||||
|
|
||||||
|
const fixedDiagnosticsCount =
|
||||||
|
diagnosticsCountBeforeFormat - diagnosticsCountAfterFormat
|
||||||
|
|
||||||
|
const updatedFilePaths: string[] = []
|
||||||
|
|
||||||
|
if (fixedDiagnosticsCount) {
|
||||||
|
updatedFilePaths.push(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updatedFilePaths,
|
||||||
|
fixedDiagnosticsCount,
|
||||||
|
unfixedDiagnostics: diagnosticsAfterFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
228
src/format/formatFolder.spec.ts
Normal file
228
src/format/formatFolder.spec.ts
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import { formatFolder } from './formatFolder'
|
||||||
|
import path from 'path'
|
||||||
|
import {
|
||||||
|
createFile,
|
||||||
|
createFolder,
|
||||||
|
deleteFolder,
|
||||||
|
readFile
|
||||||
|
} from '@sasjs/utils/file'
|
||||||
|
import { Diagnostic, LintConfig } from '../types'
|
||||||
|
|
||||||
|
describe('formatFolder', () => {
|
||||||
|
it('should fix linting issues in a given folder', async () => {
|
||||||
|
const content = `%macro somemacro(); \n%put 'hello';\n%mend;`
|
||||||
|
const expectedContent = `/**\n @file\n @brief <Your brief here>\n <h4> SAS Macros </h4>\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;`
|
||||||
|
const expectedResult = {
|
||||||
|
updatedFilePaths: [
|
||||||
|
path.join(__dirname, 'format-folder-test', 'format-folder-test.sas')
|
||||||
|
],
|
||||||
|
fixedDiagnosticsCount: 3,
|
||||||
|
unfixedDiagnostics: new Map<string, Diagnostic[]>([
|
||||||
|
[
|
||||||
|
path.join(__dirname, 'format-folder-test', 'format-folder-test.sas'),
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
await createFolder(path.join(__dirname, 'format-folder-test'))
|
||||||
|
await createFile(
|
||||||
|
path.join(__dirname, 'format-folder-test', 'format-folder-test.sas'),
|
||||||
|
content
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await formatFolder(
|
||||||
|
path.join(__dirname, 'format-folder-test')
|
||||||
|
)
|
||||||
|
const formattedContent = await readFile(
|
||||||
|
path.join(__dirname, 'format-folder-test', 'format-folder-test.sas')
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(formattedContent).toEqual(expectedContent)
|
||||||
|
expect(result).toEqual(expectedResult)
|
||||||
|
|
||||||
|
await deleteFolder(path.join(__dirname, 'format-folder-test'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fix linting issues in subfolders of a given folder', async () => {
|
||||||
|
const content = `%macro somemacro(); \n%put 'hello';\n%mend;`
|
||||||
|
const expectedContent = `/**\n @file\n @brief <Your brief here>\n <h4> SAS Macros </h4>\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;`
|
||||||
|
const expectedResult = {
|
||||||
|
updatedFilePaths: [
|
||||||
|
path.join(
|
||||||
|
__dirname,
|
||||||
|
'format-folder-test',
|
||||||
|
'subfolder',
|
||||||
|
'format-folder-test.sas'
|
||||||
|
)
|
||||||
|
],
|
||||||
|
fixedDiagnosticsCount: 3,
|
||||||
|
unfixedDiagnostics: new Map<string, Diagnostic[]>([
|
||||||
|
[
|
||||||
|
path.join(
|
||||||
|
__dirname,
|
||||||
|
'format-folder-test',
|
||||||
|
'subfolder',
|
||||||
|
'format-folder-test.sas'
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
await createFolder(path.join(__dirname, 'format-folder-test'))
|
||||||
|
await createFolder(path.join(__dirname, 'subfolder'))
|
||||||
|
await createFile(
|
||||||
|
path.join(
|
||||||
|
__dirname,
|
||||||
|
'format-folder-test',
|
||||||
|
'subfolder',
|
||||||
|
'format-folder-test.sas'
|
||||||
|
),
|
||||||
|
content
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await formatFolder(
|
||||||
|
path.join(__dirname, 'format-folder-test')
|
||||||
|
)
|
||||||
|
const formattedContent = await readFile(
|
||||||
|
path.join(
|
||||||
|
__dirname,
|
||||||
|
'format-folder-test',
|
||||||
|
'subfolder',
|
||||||
|
'format-folder-test.sas'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedResult)
|
||||||
|
expect(formattedContent).toEqual(expectedContent)
|
||||||
|
|
||||||
|
await deleteFolder(path.join(__dirname, 'format-folder-test'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use a custom configuration when provided', async () => {
|
||||||
|
const content = `%macro somemacro(); \n%put 'hello';\n%mend;`
|
||||||
|
const expectedContent = `/**\n @file\n @brief <Your brief here>\n <h4> SAS Macros </h4>\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;`
|
||||||
|
const expectedResult = {
|
||||||
|
updatedFilePaths: [
|
||||||
|
path.join(__dirname, 'format-folder-test', 'format-folder-test.sas')
|
||||||
|
],
|
||||||
|
fixedDiagnosticsCount: 3,
|
||||||
|
unfixedDiagnostics: new Map<string, Diagnostic[]>([
|
||||||
|
[
|
||||||
|
path.join(__dirname, 'format-folder-test', 'format-folder-test.sas'),
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
await createFolder(path.join(__dirname, 'format-folder-test'))
|
||||||
|
await createFile(
|
||||||
|
path.join(__dirname, 'format-folder-test', 'format-folder-test.sas'),
|
||||||
|
content
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await formatFolder(
|
||||||
|
path.join(__dirname, 'format-folder-test'),
|
||||||
|
new LintConfig({
|
||||||
|
lineEndings: 'crlf',
|
||||||
|
hasMacroNameInMend: false,
|
||||||
|
hasDoxygenHeader: true,
|
||||||
|
noTrailingSpaces: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const formattedContent = await readFile(
|
||||||
|
path.join(__dirname, 'format-folder-test', 'format-folder-test.sas')
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(formattedContent).toEqual(expectedContent)
|
||||||
|
expect(result).toEqual(expectedResult)
|
||||||
|
|
||||||
|
await deleteFolder(path.join(__dirname, 'format-folder-test'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fix linting issues in subfolders of a given folder', async () => {
|
||||||
|
const content = `%macro somemacro(); \n%put 'hello';\n%mend;`
|
||||||
|
const expectedContent = `/**\n @file\n @brief <Your brief here>\n <h4> SAS Macros </h4>\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;`
|
||||||
|
const expectedResult = {
|
||||||
|
updatedFilePaths: [
|
||||||
|
path.join(
|
||||||
|
__dirname,
|
||||||
|
'format-folder-test',
|
||||||
|
'subfolder',
|
||||||
|
'format-folder-test.sas'
|
||||||
|
)
|
||||||
|
],
|
||||||
|
fixedDiagnosticsCount: 3,
|
||||||
|
unfixedDiagnostics: new Map<string, Diagnostic[]>([
|
||||||
|
[
|
||||||
|
path.join(
|
||||||
|
__dirname,
|
||||||
|
'format-folder-test',
|
||||||
|
'subfolder',
|
||||||
|
'format-folder-test.sas'
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
await createFolder(path.join(__dirname, 'format-folder-test'))
|
||||||
|
await createFolder(path.join(__dirname, 'subfolder'))
|
||||||
|
await createFile(
|
||||||
|
path.join(
|
||||||
|
__dirname,
|
||||||
|
'format-folder-test',
|
||||||
|
'subfolder',
|
||||||
|
'format-folder-test.sas'
|
||||||
|
),
|
||||||
|
content
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await formatFolder(
|
||||||
|
path.join(__dirname, 'format-folder-test')
|
||||||
|
)
|
||||||
|
const formattedContent = await readFile(
|
||||||
|
path.join(
|
||||||
|
__dirname,
|
||||||
|
'format-folder-test',
|
||||||
|
'subfolder',
|
||||||
|
'format-folder-test.sas'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedResult)
|
||||||
|
expect(formattedContent).toEqual(expectedContent)
|
||||||
|
|
||||||
|
await deleteFolder(path.join(__dirname, 'format-folder-test'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not update any files when there are no violations', async () => {
|
||||||
|
const content = `/**\n @file\n @brief <Your brief here>\n <h4> SAS Macros </h4>\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;`
|
||||||
|
const expectedResult = {
|
||||||
|
updatedFilePaths: [],
|
||||||
|
fixedDiagnosticsCount: 0,
|
||||||
|
unfixedDiagnostics: new Map<string, Diagnostic[]>([
|
||||||
|
[
|
||||||
|
path.join(__dirname, 'format-folder-test', 'format-folder-test.sas'),
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
await createFolder(path.join(__dirname, 'format-folder-test'))
|
||||||
|
await createFile(
|
||||||
|
path.join(__dirname, 'format-folder-test', 'format-folder-test.sas'),
|
||||||
|
content
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await formatFolder(
|
||||||
|
path.join(__dirname, 'format-folder-test')
|
||||||
|
)
|
||||||
|
const formattedContent = await readFile(
|
||||||
|
path.join(__dirname, 'format-folder-test', 'format-folder-test.sas')
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(formattedContent).toEqual(content)
|
||||||
|
expect(result).toEqual(expectedResult)
|
||||||
|
|
||||||
|
await deleteFolder(path.join(__dirname, 'format-folder-test'))
|
||||||
|
})
|
||||||
|
})
|
||||||
74
src/format/formatFolder.ts
Normal file
74
src/format/formatFolder.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { listSubFoldersInFolder } from '@sasjs/utils/file'
|
||||||
|
import path from 'path'
|
||||||
|
import { lintFolder } from '../lint'
|
||||||
|
import { FormatResult } from '../types'
|
||||||
|
import { LintConfig } from '../types/LintConfig'
|
||||||
|
import { asyncForEach } from '../utils/asyncForEach'
|
||||||
|
import { getLintConfig } from '../utils/getLintConfig'
|
||||||
|
import { listSasFiles } from '../utils/listSasFiles'
|
||||||
|
import { formatFile } from './formatFile'
|
||||||
|
|
||||||
|
const excludeFolders = [
|
||||||
|
'.git',
|
||||||
|
'.github',
|
||||||
|
'.vscode',
|
||||||
|
'node_modules',
|
||||||
|
'sasjsbuild',
|
||||||
|
'sasjsresults'
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically formats all SAS files in the folder at the given path.
|
||||||
|
* @param {string} folderPath - the path to the folder to be formatted.
|
||||||
|
* @param {LintConfig} configuration - an optional configuration. When not passed in, this is read from the .sasjslint file.
|
||||||
|
* @returns {Promise<FormatResult>} Resolves successfully when all SAS files in the given folder have been formatted.
|
||||||
|
*/
|
||||||
|
export const formatFolder = async (
|
||||||
|
folderPath: string,
|
||||||
|
configuration?: LintConfig
|
||||||
|
): Promise<FormatResult> => {
|
||||||
|
const config = configuration || (await getLintConfig())
|
||||||
|
const diagnosticsBeforeFormat = await lintFolder(folderPath)
|
||||||
|
const diagnosticsCountBeforeFormat = Array.from(
|
||||||
|
diagnosticsBeforeFormat.values()
|
||||||
|
).reduce((a, b) => a + b.length, 0)
|
||||||
|
|
||||||
|
const fileNames = await listSasFiles(folderPath)
|
||||||
|
await asyncForEach(fileNames, async (fileName) => {
|
||||||
|
const filePath = path.join(folderPath, fileName)
|
||||||
|
await formatFile(filePath)
|
||||||
|
})
|
||||||
|
|
||||||
|
const subFolders = (await listSubFoldersInFolder(folderPath)).filter(
|
||||||
|
(f: string) => !excludeFolders.includes(f)
|
||||||
|
)
|
||||||
|
|
||||||
|
await asyncForEach(subFolders, async (subFolder) => {
|
||||||
|
await formatFolder(path.join(folderPath, subFolder), config)
|
||||||
|
})
|
||||||
|
|
||||||
|
const diagnosticsAfterFormat = await lintFolder(folderPath)
|
||||||
|
const diagnosticsCountAfterFormat = Array.from(
|
||||||
|
diagnosticsAfterFormat.values()
|
||||||
|
).reduce((a, b) => a + b.length, 0)
|
||||||
|
|
||||||
|
const fixedDiagnosticsCount =
|
||||||
|
diagnosticsCountBeforeFormat - diagnosticsCountAfterFormat
|
||||||
|
|
||||||
|
const updatedFilePaths: string[] = []
|
||||||
|
|
||||||
|
Array.from(diagnosticsBeforeFormat.keys()).forEach((filePath) => {
|
||||||
|
const diagnosticsBefore = diagnosticsBeforeFormat.get(filePath) || []
|
||||||
|
const diagnosticsAfter = diagnosticsAfterFormat.get(filePath) || []
|
||||||
|
|
||||||
|
if (diagnosticsBefore.length !== diagnosticsAfter.length) {
|
||||||
|
updatedFilePaths.push(filePath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
updatedFilePaths,
|
||||||
|
fixedDiagnosticsCount,
|
||||||
|
unfixedDiagnostics: diagnosticsAfterFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/format/formatProject.spec.ts
Normal file
51
src/format/formatProject.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { formatProject } from './formatProject'
|
||||||
|
import path from 'path'
|
||||||
|
import {
|
||||||
|
createFile,
|
||||||
|
createFolder,
|
||||||
|
deleteFolder,
|
||||||
|
readFile
|
||||||
|
} from '@sasjs/utils/file'
|
||||||
|
import { DefaultLintConfiguration } from '../utils'
|
||||||
|
import * as getProjectRootModule from '../utils/getProjectRoot'
|
||||||
|
jest.mock('../utils/getProjectRoot')
|
||||||
|
|
||||||
|
describe('formatProject', () => {
|
||||||
|
it('should format files in the current project', async () => {
|
||||||
|
const content = `%macro somemacro(); \n%put 'hello';\n%mend;`
|
||||||
|
const expectedContent = `/**\n @file\n @brief <Your brief here>\n <h4> SAS Macros </h4>\n**/\n%macro somemacro();\n%put 'hello';\n%mend somemacro;`
|
||||||
|
await createFolder(path.join(__dirname, 'format-project-test'))
|
||||||
|
await createFile(
|
||||||
|
path.join(__dirname, 'format-project-test', 'format-project-test.sas'),
|
||||||
|
content
|
||||||
|
)
|
||||||
|
await createFile(
|
||||||
|
path.join(__dirname, 'format-project-test', '.sasjslint'),
|
||||||
|
JSON.stringify(DefaultLintConfiguration)
|
||||||
|
)
|
||||||
|
jest
|
||||||
|
.spyOn(getProjectRootModule, 'getProjectRoot')
|
||||||
|
.mockImplementation(() =>
|
||||||
|
Promise.resolve(path.join(__dirname, 'format-project-test'))
|
||||||
|
)
|
||||||
|
|
||||||
|
await formatProject()
|
||||||
|
const result = await readFile(
|
||||||
|
path.join(__dirname, 'format-project-test', 'format-project-test.sas')
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedContent)
|
||||||
|
|
||||||
|
await deleteFolder(path.join(__dirname, 'format-project-test'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw an error when a project root is not found', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(getProjectRootModule, 'getProjectRoot')
|
||||||
|
.mockImplementationOnce(() => Promise.resolve(''))
|
||||||
|
|
||||||
|
await expect(formatProject()).rejects.toThrowError(
|
||||||
|
'SASjs Project Root was not found.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
18
src/format/formatProject.ts
Normal file
18
src/format/formatProject.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { lintFolder } from '../lint/lintFolder'
|
||||||
|
import { FormatResult } from '../types/FormatResult'
|
||||||
|
import { getProjectRoot } from '../utils/getProjectRoot'
|
||||||
|
import { formatFolder } from './formatFolder'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically formats all SAS files in the current project.
|
||||||
|
* @returns {Promise<FormatResult>} Resolves successfully when all SAS files in the current project have been formatted.
|
||||||
|
*/
|
||||||
|
export const formatProject = async (): Promise<FormatResult> => {
|
||||||
|
const projectRoot =
|
||||||
|
(await getProjectRoot()) || process.projectDir || process.currentDir
|
||||||
|
if (!projectRoot) {
|
||||||
|
throw new Error('SASjs Project Root was not found.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return await formatFolder(projectRoot)
|
||||||
|
}
|
||||||
49
src/format/formatText.spec.ts
Normal file
49
src/format/formatText.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { formatText } from './formatText'
|
||||||
|
import * as getLintConfigModule from '../utils/getLintConfig'
|
||||||
|
import { LintConfig } from '../types'
|
||||||
|
jest.mock('../utils/getLintConfig')
|
||||||
|
|
||||||
|
describe('formatText', () => {
|
||||||
|
it('should format the given text based on configured rules', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(getLintConfigModule, 'getLintConfig')
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve(
|
||||||
|
new LintConfig(getLintConfigModule.DefaultLintConfiguration)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const text = `%macro test;
|
||||||
|
%put 'hello';\r\n%mend; `
|
||||||
|
|
||||||
|
const expectedOutput = `/**
|
||||||
|
@file
|
||||||
|
@brief <Your brief here>
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
**/\n%macro test;
|
||||||
|
%put 'hello';\n%mend test;`
|
||||||
|
|
||||||
|
const output = await formatText(text)
|
||||||
|
|
||||||
|
expect(output).toEqual(expectedOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use CRLF line endings when configured', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(getLintConfigModule, 'getLintConfig')
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve(
|
||||||
|
new LintConfig({
|
||||||
|
...getLintConfigModule.DefaultLintConfiguration,
|
||||||
|
lineEndings: 'crlf'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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 output = await formatText(text)
|
||||||
|
|
||||||
|
expect(output).toEqual(expectedOutput)
|
||||||
|
})
|
||||||
|
})
|
||||||
7
src/format/formatText.ts
Normal file
7
src/format/formatText.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { getLintConfig } from '../utils'
|
||||||
|
import { processText } from './shared'
|
||||||
|
|
||||||
|
export const formatText = async (text: string) => {
|
||||||
|
const config = await getLintConfig()
|
||||||
|
return processText(text, config)
|
||||||
|
}
|
||||||
4
src/format/index.ts
Normal file
4
src/format/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './formatText'
|
||||||
|
export * from './formatFile'
|
||||||
|
export * from './formatFolder'
|
||||||
|
export * from './formatProject'
|
||||||
37
src/format/shared.ts
Normal file
37
src/format/shared.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { LintConfig } from '../types'
|
||||||
|
import { LineEndings } from '../types/LineEndings'
|
||||||
|
import { splitText } from '../utils/splitText'
|
||||||
|
|
||||||
|
export const processText = (text: string, config: LintConfig) => {
|
||||||
|
const processedText = processContent(config, text)
|
||||||
|
const lines = splitText(processedText, config)
|
||||||
|
const formattedLines = lines.map((line) => {
|
||||||
|
return processLine(config, line)
|
||||||
|
})
|
||||||
|
|
||||||
|
const configuredLineEnding =
|
||||||
|
config.lineEndings === LineEndings.LF ? '\n' : '\r\n'
|
||||||
|
return formattedLines.join(configuredLineEnding)
|
||||||
|
}
|
||||||
|
|
||||||
|
const processContent = (config: LintConfig, content: string): string => {
|
||||||
|
let processedContent = content
|
||||||
|
config.fileLintRules
|
||||||
|
.filter((r) => !!r.fix)
|
||||||
|
.forEach((rule) => {
|
||||||
|
processedContent = rule.fix!(processedContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
return processedContent
|
||||||
|
}
|
||||||
|
|
||||||
|
export const processLine = (config: LintConfig, line: string): string => {
|
||||||
|
let processedLine = line
|
||||||
|
config.lineLintRules
|
||||||
|
.filter((r) => !!r.fix)
|
||||||
|
.forEach((rule) => {
|
||||||
|
processedLine = rule.fix!(line)
|
||||||
|
})
|
||||||
|
|
||||||
|
return processedLine
|
||||||
|
}
|
||||||
21
src/formatExample.ts
Normal file
21
src/formatExample.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { formatText } from './format/formatText'
|
||||||
|
import { lintText } from './lint'
|
||||||
|
|
||||||
|
const content = `%put 'Hello';
|
||||||
|
%put 'World';
|
||||||
|
%macro somemacro()
|
||||||
|
%put 'test';
|
||||||
|
%mend;\r\n`
|
||||||
|
|
||||||
|
console.log(content)
|
||||||
|
lintText(content).then((diagnostics) => {
|
||||||
|
console.log('Before Formatting:')
|
||||||
|
console.table(diagnostics)
|
||||||
|
formatText(content).then((formattedText) => {
|
||||||
|
lintText(formattedText).then((newDiagnostics) => {
|
||||||
|
console.log('After Formatting:')
|
||||||
|
console.log(formattedText)
|
||||||
|
console.table(newDiagnostics)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from './format'
|
||||||
export * from './lint'
|
export * from './lint'
|
||||||
export * from './types'
|
export * from './types'
|
||||||
export * from './utils'
|
export * from './utils'
|
||||||
|
|||||||
@@ -2,68 +2,87 @@ import { lintFile } from './lintFile'
|
|||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../types/Severity'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
|
const expectedDiagnostics = [
|
||||||
|
{
|
||||||
|
message: 'Line contains trailing spaces',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 2,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Line contains trailing spaces',
|
||||||
|
lineNumber: 2,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 2,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'File name contains spaces',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'File name contains uppercase characters',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'File missing Doxygen header',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Line contains encoded password',
|
||||||
|
lineNumber: 5,
|
||||||
|
startColumnNumber: 10,
|
||||||
|
endColumnNumber: 18,
|
||||||
|
severity: Severity.Error
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Line is indented with a tab',
|
||||||
|
lineNumber: 7,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Line has incorrect indentation - 3 spaces',
|
||||||
|
lineNumber: 6,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: '%mend statement is missing macro name - mf_getuniquelibref',
|
||||||
|
lineNumber: 17,
|
||||||
|
startColumnNumber: 3,
|
||||||
|
endColumnNumber: 9,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
describe('lintFile', () => {
|
describe('lintFile', () => {
|
||||||
it('should identify lint issues in a given file', async () => {
|
it('should identify lint issues in a given file', async () => {
|
||||||
const results = await lintFile(
|
const results = await lintFile(
|
||||||
path.join(__dirname, '..', 'Example File.sas')
|
path.join(__dirname, '..', 'Example File.sas')
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(results.length).toEqual(8)
|
expect(results.length).toEqual(expectedDiagnostics.length)
|
||||||
expect(results).toContainEqual({
|
expect(results).toContainEqual(expectedDiagnostics[0])
|
||||||
message: 'Line contains trailing spaces',
|
expect(results).toContainEqual(expectedDiagnostics[1])
|
||||||
lineNumber: 1,
|
expect(results).toContainEqual(expectedDiagnostics[2])
|
||||||
startColumnNumber: 1,
|
expect(results).toContainEqual(expectedDiagnostics[3])
|
||||||
endColumnNumber: 2,
|
expect(results).toContainEqual(expectedDiagnostics[4])
|
||||||
severity: Severity.Warning
|
expect(results).toContainEqual(expectedDiagnostics[5])
|
||||||
})
|
expect(results).toContainEqual(expectedDiagnostics[6])
|
||||||
expect(results).toContainEqual({
|
expect(results).toContainEqual(expectedDiagnostics[7])
|
||||||
message: 'Line contains trailing spaces',
|
expect(results).toContainEqual(expectedDiagnostics[8])
|
||||||
lineNumber: 2,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 2,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
expect(results).toContainEqual({
|
|
||||||
message: 'File name contains spaces',
|
|
||||||
lineNumber: 1,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
expect(results).toContainEqual({
|
|
||||||
message: 'File name contains uppercase characters',
|
|
||||||
lineNumber: 1,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
expect(results).toContainEqual({
|
|
||||||
message: 'File missing Doxygen header',
|
|
||||||
lineNumber: 1,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
expect(results).toContainEqual({
|
|
||||||
message: 'Line contains encoded password',
|
|
||||||
lineNumber: 5,
|
|
||||||
startColumnNumber: 10,
|
|
||||||
endColumnNumber: 18,
|
|
||||||
severity: Severity.Error
|
|
||||||
})
|
|
||||||
expect(results).toContainEqual({
|
|
||||||
message: 'Line is indented with a tab',
|
|
||||||
lineNumber: 7,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
expect(results).toContainEqual({
|
|
||||||
message: 'Line has incorrect indentation - 3 spaces',
|
|
||||||
lineNumber: 6,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,71 +1,135 @@
|
|||||||
import { lintFolder } from './lintFolder'
|
import { lintFolder } from './lintFolder'
|
||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../types/Severity'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import {
|
||||||
|
createFile,
|
||||||
|
createFolder,
|
||||||
|
deleteFolder,
|
||||||
|
readFile
|
||||||
|
} from '@sasjs/utils/file'
|
||||||
|
|
||||||
|
const expectedFilesCount = 1
|
||||||
|
const expectedDiagnostics = [
|
||||||
|
{
|
||||||
|
message: 'Line contains trailing spaces',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 2,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Line contains trailing spaces',
|
||||||
|
lineNumber: 2,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 2,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'File name contains spaces',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'File name contains uppercase characters',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'File missing Doxygen header',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Line contains encoded password',
|
||||||
|
lineNumber: 5,
|
||||||
|
startColumnNumber: 10,
|
||||||
|
endColumnNumber: 18,
|
||||||
|
severity: Severity.Error
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Line is indented with a tab',
|
||||||
|
lineNumber: 7,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Line has incorrect indentation - 3 spaces',
|
||||||
|
lineNumber: 6,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: '%mend statement is missing macro name - mf_getuniquelibref',
|
||||||
|
lineNumber: 17,
|
||||||
|
startColumnNumber: 3,
|
||||||
|
endColumnNumber: 9,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
describe('lintFolder', () => {
|
describe('lintFolder', () => {
|
||||||
it('should identify lint issues in a given folder', async () => {
|
it('should identify lint issues in a given folder', async () => {
|
||||||
const results = await lintFolder(path.join(__dirname, '..'))
|
await createFolder(path.join(__dirname, 'lint-folder-test'))
|
||||||
|
const content = await readFile(
|
||||||
expect(results.size).toEqual(1)
|
|
||||||
const diagnostics = results.get(
|
|
||||||
path.join(__dirname, '..', 'Example File.sas')
|
path.join(__dirname, '..', 'Example File.sas')
|
||||||
|
)
|
||||||
|
await createFile(
|
||||||
|
path.join(__dirname, 'lint-folder-test', 'Example File.sas'),
|
||||||
|
content
|
||||||
|
)
|
||||||
|
const results = await lintFolder(path.join(__dirname, 'lint-folder-test'))
|
||||||
|
expect(results.size).toEqual(expectedFilesCount)
|
||||||
|
const diagnostics = results.get(
|
||||||
|
path.join(__dirname, 'lint-folder-test', 'Example File.sas')
|
||||||
)!
|
)!
|
||||||
expect(diagnostics.length).toEqual(8)
|
expect(diagnostics.length).toEqual(expectedDiagnostics.length)
|
||||||
expect(diagnostics).toContainEqual({
|
expect(diagnostics).toContainEqual(expectedDiagnostics[0])
|
||||||
message: 'Line contains trailing spaces',
|
expect(diagnostics).toContainEqual(expectedDiagnostics[1])
|
||||||
lineNumber: 1,
|
expect(diagnostics).toContainEqual(expectedDiagnostics[2])
|
||||||
startColumnNumber: 1,
|
expect(diagnostics).toContainEqual(expectedDiagnostics[3])
|
||||||
endColumnNumber: 2,
|
expect(diagnostics).toContainEqual(expectedDiagnostics[4])
|
||||||
severity: Severity.Warning
|
expect(diagnostics).toContainEqual(expectedDiagnostics[5])
|
||||||
})
|
expect(diagnostics).toContainEqual(expectedDiagnostics[6])
|
||||||
expect(diagnostics).toContainEqual({
|
expect(diagnostics).toContainEqual(expectedDiagnostics[7])
|
||||||
message: 'Line contains trailing spaces',
|
expect(diagnostics).toContainEqual(expectedDiagnostics[8])
|
||||||
lineNumber: 2,
|
|
||||||
startColumnNumber: 1,
|
await deleteFolder(path.join(__dirname, 'lint-folder-test'))
|
||||||
endColumnNumber: 2,
|
})
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
it('should identify lint issues in subfolders of a given folder', async () => {
|
||||||
expect(diagnostics).toContainEqual({
|
await createFolder(path.join(__dirname, 'lint-folder-test'))
|
||||||
message: 'File name contains spaces',
|
await createFolder(path.join(__dirname, 'lint-folder-test', 'subfolder'))
|
||||||
lineNumber: 1,
|
const content = await readFile(
|
||||||
startColumnNumber: 1,
|
path.join(__dirname, '..', 'Example File.sas')
|
||||||
endColumnNumber: 1,
|
)
|
||||||
severity: Severity.Warning
|
await createFile(
|
||||||
})
|
path.join(__dirname, 'lint-folder-test', 'subfolder', 'Example File.sas'),
|
||||||
expect(diagnostics).toContainEqual({
|
content
|
||||||
message: 'File name contains uppercase characters',
|
)
|
||||||
lineNumber: 1,
|
const results = await lintFolder(path.join(__dirname, 'lint-folder-test'))
|
||||||
startColumnNumber: 1,
|
expect(results.size).toEqual(expectedFilesCount)
|
||||||
endColumnNumber: 1,
|
const diagnostics = results.get(
|
||||||
severity: Severity.Warning
|
path.join(__dirname, 'lint-folder-test', 'subfolder', 'Example File.sas')
|
||||||
})
|
)!
|
||||||
expect(diagnostics).toContainEqual({
|
expect(diagnostics.length).toEqual(expectedDiagnostics.length)
|
||||||
message: 'File missing Doxygen header',
|
expect(diagnostics).toContainEqual(expectedDiagnostics[0])
|
||||||
lineNumber: 1,
|
expect(diagnostics).toContainEqual(expectedDiagnostics[1])
|
||||||
startColumnNumber: 1,
|
expect(diagnostics).toContainEqual(expectedDiagnostics[2])
|
||||||
endColumnNumber: 1,
|
expect(diagnostics).toContainEqual(expectedDiagnostics[3])
|
||||||
severity: Severity.Warning
|
expect(diagnostics).toContainEqual(expectedDiagnostics[4])
|
||||||
})
|
expect(diagnostics).toContainEqual(expectedDiagnostics[5])
|
||||||
expect(diagnostics).toContainEqual({
|
expect(diagnostics).toContainEqual(expectedDiagnostics[6])
|
||||||
message: 'Line contains encoded password',
|
expect(diagnostics).toContainEqual(expectedDiagnostics[7])
|
||||||
lineNumber: 5,
|
expect(diagnostics).toContainEqual(expectedDiagnostics[8])
|
||||||
startColumnNumber: 10,
|
|
||||||
endColumnNumber: 18,
|
await deleteFolder(path.join(__dirname, 'lint-folder-test'))
|
||||||
severity: Severity.Error
|
|
||||||
})
|
|
||||||
expect(diagnostics).toContainEqual({
|
|
||||||
message: 'Line is indented with a tab',
|
|
||||||
lineNumber: 7,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
expect(diagnostics).toContainEqual({
|
|
||||||
message: 'Line has incorrect indentation - 3 spaces',
|
|
||||||
lineNumber: 6,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,82 +1,121 @@
|
|||||||
import { lintProject } from './lintProject'
|
import { lintProject } from './lintProject'
|
||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../types/Severity'
|
||||||
import * as utils from '../utils'
|
import * as getProjectRootModule from '../utils/getProjectRoot'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
jest.mock('../utils')
|
import { createFolder, createFile, readFile, deleteFolder } from '@sasjs/utils'
|
||||||
|
import { DefaultLintConfiguration } from '../utils'
|
||||||
|
jest.mock('../utils/getProjectRoot')
|
||||||
|
|
||||||
|
const expectedFilesCount = 1
|
||||||
|
const expectedDiagnostics = [
|
||||||
|
{
|
||||||
|
message: 'Line contains trailing spaces',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 2,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Line contains trailing spaces',
|
||||||
|
lineNumber: 2,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 2,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'File name contains spaces',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'File name contains uppercase characters',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'File missing Doxygen header',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Line contains encoded password',
|
||||||
|
lineNumber: 5,
|
||||||
|
startColumnNumber: 10,
|
||||||
|
endColumnNumber: 18,
|
||||||
|
severity: Severity.Error
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Line is indented with a tab',
|
||||||
|
lineNumber: 7,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Line has incorrect indentation - 3 spaces',
|
||||||
|
lineNumber: 6,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: '%mend statement is missing macro name - mf_getuniquelibref',
|
||||||
|
lineNumber: 17,
|
||||||
|
startColumnNumber: 3,
|
||||||
|
endColumnNumber: 9,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
describe('lintProject', () => {
|
describe('lintProject', () => {
|
||||||
it('should identify lint issues in a given project', async () => {
|
it('should identify lint issues in a given project', async () => {
|
||||||
|
await createFolder(path.join(__dirname, 'lint-project-test'))
|
||||||
|
const content = await readFile(
|
||||||
|
path.join(__dirname, '..', 'Example File.sas')
|
||||||
|
)
|
||||||
|
await createFile(
|
||||||
|
path.join(__dirname, 'lint-project-test', 'Example File.sas'),
|
||||||
|
content
|
||||||
|
)
|
||||||
|
await createFile(
|
||||||
|
path.join(__dirname, 'lint-project-test', '.sasjslint'),
|
||||||
|
JSON.stringify(DefaultLintConfiguration)
|
||||||
|
)
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(utils, 'getProjectRoot')
|
.spyOn(getProjectRootModule, 'getProjectRoot')
|
||||||
.mockImplementationOnce(() => Promise.resolve(path.join(__dirname, '..')))
|
.mockImplementation(() =>
|
||||||
|
Promise.resolve(path.join(__dirname, 'lint-project-test'))
|
||||||
|
)
|
||||||
const results = await lintProject()
|
const results = await lintProject()
|
||||||
|
|
||||||
expect(results.size).toEqual(1)
|
expect(results.size).toEqual(expectedFilesCount)
|
||||||
const diagnostics = results.get(
|
const diagnostics = results.get(
|
||||||
path.join(__dirname, '..', 'Example File.sas')
|
path.join(__dirname, 'lint-project-test', 'Example File.sas')
|
||||||
)!
|
)!
|
||||||
expect(diagnostics.length).toEqual(8)
|
expect(diagnostics.length).toEqual(expectedDiagnostics.length)
|
||||||
expect(diagnostics).toContainEqual({
|
expect(diagnostics).toContainEqual(expectedDiagnostics[0])
|
||||||
message: 'Line contains trailing spaces',
|
expect(diagnostics).toContainEqual(expectedDiagnostics[1])
|
||||||
lineNumber: 1,
|
expect(diagnostics).toContainEqual(expectedDiagnostics[2])
|
||||||
startColumnNumber: 1,
|
expect(diagnostics).toContainEqual(expectedDiagnostics[3])
|
||||||
endColumnNumber: 2,
|
expect(diagnostics).toContainEqual(expectedDiagnostics[4])
|
||||||
severity: Severity.Warning
|
expect(diagnostics).toContainEqual(expectedDiagnostics[5])
|
||||||
})
|
expect(diagnostics).toContainEqual(expectedDiagnostics[6])
|
||||||
expect(diagnostics).toContainEqual({
|
expect(diagnostics).toContainEqual(expectedDiagnostics[7])
|
||||||
message: 'Line contains trailing spaces',
|
expect(diagnostics).toContainEqual(expectedDiagnostics[8])
|
||||||
lineNumber: 2,
|
|
||||||
startColumnNumber: 1,
|
await deleteFolder(path.join(__dirname, 'lint-project-test'))
|
||||||
endColumnNumber: 2,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
expect(diagnostics).toContainEqual({
|
|
||||||
message: 'File name contains spaces',
|
|
||||||
lineNumber: 1,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
expect(diagnostics).toContainEqual({
|
|
||||||
message: 'File name contains uppercase characters',
|
|
||||||
lineNumber: 1,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
expect(diagnostics).toContainEqual({
|
|
||||||
message: 'File missing Doxygen header',
|
|
||||||
lineNumber: 1,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
expect(diagnostics).toContainEqual({
|
|
||||||
message: 'Line contains encoded password',
|
|
||||||
lineNumber: 5,
|
|
||||||
startColumnNumber: 10,
|
|
||||||
endColumnNumber: 18,
|
|
||||||
severity: Severity.Error
|
|
||||||
})
|
|
||||||
expect(diagnostics).toContainEqual({
|
|
||||||
message: 'Line is indented with a tab',
|
|
||||||
lineNumber: 7,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
expect(diagnostics).toContainEqual({
|
|
||||||
message: 'Line has incorrect indentation - 3 spaces',
|
|
||||||
lineNumber: 6,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw an error when a project root is not found', async () => {
|
it('should throw an error when a project root is not found', async () => {
|
||||||
jest
|
jest
|
||||||
.spyOn(utils, 'getProjectRoot')
|
.spyOn(getProjectRootModule, 'getProjectRoot')
|
||||||
.mockImplementationOnce(() => Promise.resolve(''))
|
.mockImplementationOnce(() => Promise.resolve(''))
|
||||||
|
|
||||||
await expect(lintProject()).rejects.toThrowError(
|
await expect(lintProject()).rejects.toThrowError(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getProjectRoot } from '../utils'
|
import { getProjectRoot } from '../utils/getProjectRoot'
|
||||||
import { lintFolder } from './lintFolder'
|
import { lintFolder } from './lintFolder'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8,7 +8,6 @@ import { lintFolder } from './lintFolder'
|
|||||||
export const lintProject = async () => {
|
export const lintProject = async () => {
|
||||||
const projectRoot =
|
const projectRoot =
|
||||||
(await getProjectRoot()) || process.projectDir || process.currentDir
|
(await getProjectRoot()) || process.projectDir || process.currentDir
|
||||||
|
|
||||||
if (!projectRoot) {
|
if (!projectRoot) {
|
||||||
throw new Error('SASjs Project Root was not found.')
|
throw new Error('SASjs Project Root was not found.')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { splitText } from './shared'
|
|
||||||
|
|
||||||
describe('splitText', () => {
|
|
||||||
it('should return an empty array when text is falsy', () => {
|
|
||||||
const lines = splitText('')
|
|
||||||
|
|
||||||
expect(lines.length).toEqual(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return an array of lines from text', () => {
|
|
||||||
const lines = splitText(`line 1\nline 2`)
|
|
||||||
|
|
||||||
expect(lines.length).toEqual(2)
|
|
||||||
expect(lines[0]).toEqual('line 1')
|
|
||||||
expect(lines[1]).toEqual('line 2')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should work with CRLF line endings', () => {
|
|
||||||
const lines = splitText(`line 1\r\nline 2`)
|
|
||||||
|
|
||||||
expect(lines.length).toEqual(2)
|
|
||||||
expect(lines[0]).toEqual('line 1')
|
|
||||||
expect(lines[1]).toEqual('line 2')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,17 +1,8 @@
|
|||||||
import { LintConfig, Diagnostic } from '../types'
|
import { LintConfig, Diagnostic } from '../types'
|
||||||
|
import { splitText } from '../utils'
|
||||||
/**
|
|
||||||
* Splits the given content into a list of lines, regardless of CRLF or LF line endings.
|
|
||||||
* @param {string} text - the text content to be split into lines.
|
|
||||||
* @returns {string[]} an array of lines from the given text
|
|
||||||
*/
|
|
||||||
export const splitText = (text: string): string[] => {
|
|
||||||
if (!text) return []
|
|
||||||
return text.replace(/\r\n/g, '\n').split('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
export const processText = (text: string, config: LintConfig) => {
|
export const processText = (text: string, config: LintConfig) => {
|
||||||
const lines = splitText(text)
|
const lines = splitText(text, config)
|
||||||
const diagnostics: Diagnostic[] = []
|
const diagnostics: Diagnostic[] = []
|
||||||
diagnostics.push(...processContent(config, text))
|
diagnostics.push(...processContent(config, text))
|
||||||
lines.forEach((line, index) => {
|
lines.forEach((line, index) => {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Severity } from '../types/Severity'
|
import { LintConfig } from '../../types'
|
||||||
|
import { Severity } from '../../types/Severity'
|
||||||
import { hasDoxygenHeader } from './hasDoxygenHeader'
|
import { hasDoxygenHeader } from './hasDoxygenHeader'
|
||||||
|
|
||||||
describe('hasDoxygenHeader', () => {
|
describe('hasDoxygenHeader - test', () => {
|
||||||
it('should return an empty array when the file starts with a doxygen header', () => {
|
it('should return an empty array when the file starts with a doxygen header', () => {
|
||||||
const content = `/**
|
const content = `/**
|
||||||
@file
|
@file
|
||||||
@@ -58,7 +59,7 @@ describe('hasDoxygenHeader', () => {
|
|||||||
it('should return an array with a single diagnostic when the file is undefined', () => {
|
it('should return an array with a single diagnostic when the file is undefined', () => {
|
||||||
const content = 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',
|
message: 'File missing Doxygen header',
|
||||||
lineNumber: 1,
|
lineNumber: 1,
|
||||||
@@ -69,3 +70,47 @@ describe('hasDoxygenHeader', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('hasDoxygenHeader - fix', () => {
|
||||||
|
it('should not alter the text if a doxygen header is already present', () => {
|
||||||
|
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.fix!(content)).toEqual(content)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should should add a doxygen header if not present', () => {
|
||||||
|
const content = `%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
|
||||||
|
%local x libref;
|
||||||
|
%let x={SAS002};
|
||||||
|
%do x=0 %to &maxtries;`
|
||||||
|
|
||||||
|
expect(hasDoxygenHeader.fix!(content)).toEqual(
|
||||||
|
`/**
|
||||||
|
@file
|
||||||
|
@brief <Your brief here>
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
**/` +
|
||||||
|
'\n' +
|
||||||
|
content
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use CRLF line endings when configured', () => {
|
||||||
|
const content = `%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);\n%local x libref;\n%let x={SAS002};\n%do x=0 %to &maxtries;`
|
||||||
|
const config = new LintConfig({ lineEndings: 'crlf' })
|
||||||
|
|
||||||
|
expect(hasDoxygenHeader.fix!(content, config)).toEqual(
|
||||||
|
`/**\r\n @file\r\n @brief <Your brief here>\r\n <h4> SAS Macros </h4>\r\n**/` +
|
||||||
|
'\r\n' +
|
||||||
|
content
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
import { FileLintRule } from '../types/LintRule'
|
import { LintConfig } from '../../types'
|
||||||
import { LintRuleType } from '../types/LintRuleType'
|
import { LineEndings } from '../../types/LineEndings'
|
||||||
import { Severity } from '../types/Severity'
|
import { FileLintRule } from '../../types/LintRule'
|
||||||
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
|
import { Severity } from '../../types/Severity'
|
||||||
|
|
||||||
|
const DoxygenHeader = `/**{lineEnding} @file{lineEnding} @brief <Your brief here>{lineEnding} <h4> SAS Macros </h4>{lineEnding}**/`
|
||||||
|
|
||||||
const name = 'hasDoxygenHeader'
|
const name = 'hasDoxygenHeader'
|
||||||
const description =
|
const description =
|
||||||
@@ -32,6 +36,19 @@ const test = (value: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fix = (value: string, config?: LintConfig): string => {
|
||||||
|
if (test(value).length === 0) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
const lineEndingConfig = config?.lineEndings || LineEndings.LF
|
||||||
|
const lineEnding = lineEndingConfig === LineEndings.LF ? '\n' : '\r\n'
|
||||||
|
|
||||||
|
return `${DoxygenHeader.replace(
|
||||||
|
/{lineEnding}/g,
|
||||||
|
lineEnding
|
||||||
|
)}${lineEnding}${value}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lint rule that checks for the presence of a Doxygen header in a given file.
|
* Lint rule that checks for the presence of a Doxygen header in a given file.
|
||||||
*/
|
*/
|
||||||
@@ -40,5 +57,6 @@ export const hasDoxygenHeader: FileLintRule = {
|
|||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
message,
|
message,
|
||||||
test
|
test,
|
||||||
|
fix
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Severity } from '../types/Severity'
|
import { LintConfig } from '../../types'
|
||||||
|
import { Severity } from '../../types/Severity'
|
||||||
import { hasMacroNameInMend } from './hasMacroNameInMend'
|
import { hasMacroNameInMend } from './hasMacroNameInMend'
|
||||||
|
|
||||||
describe('hasMacroNameInMend', () => {
|
describe('hasMacroNameInMend - test', () => {
|
||||||
it('should return an empty array when %mend has correct macro name', () => {
|
it('should return an empty array when %mend has correct macro name', () => {
|
||||||
const content = `
|
const content = `
|
||||||
%macro somemacro();
|
%macro somemacro();
|
||||||
@@ -55,7 +56,7 @@ describe('hasMacroNameInMend', () => {
|
|||||||
it('should return an array with a diagnostic for each macro missing an %mend statement', () => {
|
it('should return an array with a diagnostic for each macro missing an %mend statement', () => {
|
||||||
const content = `%macro somemacro;
|
const content = `%macro somemacro;
|
||||||
%put &sysmacroname;
|
%put &sysmacroname;
|
||||||
%macro othermacro`
|
%macro othermacro;`
|
||||||
|
|
||||||
expect(hasMacroNameInMend.test(content)).toEqual([
|
expect(hasMacroNameInMend.test(content)).toEqual([
|
||||||
{
|
{
|
||||||
@@ -113,7 +114,7 @@ describe('hasMacroNameInMend', () => {
|
|||||||
it('should return an empty array when the file is undefined', () => {
|
it('should return an empty array when the file is undefined', () => {
|
||||||
const content = undefined
|
const content = undefined
|
||||||
|
|
||||||
expect(hasMacroNameInMend.test((content as unknown) as string)).toEqual([])
|
expect(hasMacroNameInMend.test(content as unknown as string)).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('nestedMacros', () => {
|
describe('nestedMacros', () => {
|
||||||
@@ -319,4 +320,146 @@ describe('hasMacroNameInMend', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should use the configured line ending while testing content', () => {
|
||||||
|
const content = `%macro somemacro();\r\n%put &sysmacroname;\r\n%mend;`
|
||||||
|
|
||||||
|
const diagnostics = hasMacroNameInMend.test(
|
||||||
|
content,
|
||||||
|
new LintConfig({ lineEndings: 'crlf' })
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(diagnostics).toEqual([
|
||||||
|
{
|
||||||
|
message: '%mend statement is missing macro name - somemacro',
|
||||||
|
lineNumber: 3,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 7,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('hasMacroNameInMend - fix', () => {
|
||||||
|
it('should add macro name to the mend statement if not present', () => {
|
||||||
|
const content = ` %macro somemacro;\n %put &sysmacroname;\n %mend;`
|
||||||
|
const expectedContent = ` %macro somemacro;\n %put &sysmacroname;\n %mend somemacro;`
|
||||||
|
|
||||||
|
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
|
||||||
|
|
||||||
|
expect(formattedContent).toEqual(expectedContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add macro name to the mend statement if not present ( code in single line )', () => {
|
||||||
|
const content = `%macro somemacro; %put &sysmacroname; %mend; some code;`
|
||||||
|
const expectedContent = `%macro somemacro; %put &sysmacroname; %mend somemacro; some code;`
|
||||||
|
|
||||||
|
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
|
||||||
|
|
||||||
|
expect(formattedContent).toEqual(expectedContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add macro name to the mend statement if not present ( with multiple macros )', () => {
|
||||||
|
const content = `
|
||||||
|
%macro somemacro;
|
||||||
|
%put &sysmacroname;
|
||||||
|
%mend somemacro;
|
||||||
|
|
||||||
|
%macro somemacro2;
|
||||||
|
%put &sysmacroname2;
|
||||||
|
%mend;`
|
||||||
|
const expectedContent = `
|
||||||
|
%macro somemacro;
|
||||||
|
%put &sysmacroname;
|
||||||
|
%mend somemacro;
|
||||||
|
|
||||||
|
%macro somemacro2;
|
||||||
|
%put &sysmacroname2;
|
||||||
|
%mend somemacro2;`
|
||||||
|
|
||||||
|
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
|
||||||
|
|
||||||
|
expect(formattedContent).toEqual(expectedContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove redundant %mend statement', () => {
|
||||||
|
const content = `
|
||||||
|
%macro somemacro;
|
||||||
|
%put &sysmacroname;
|
||||||
|
%mend somemacro;
|
||||||
|
%mend something;`
|
||||||
|
const expectedContent = `
|
||||||
|
%macro somemacro;
|
||||||
|
%put &sysmacroname;
|
||||||
|
%mend somemacro;
|
||||||
|
`
|
||||||
|
|
||||||
|
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
|
||||||
|
expect(formattedContent).toEqual(expectedContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove redundant %mend statement with comments', () => {
|
||||||
|
const content = `
|
||||||
|
%macro somemacro;
|
||||||
|
%put &sysmacroname;
|
||||||
|
%mend somemacro;
|
||||||
|
/* some comment */
|
||||||
|
/* some comment */ %mend something; some code;
|
||||||
|
/* some comment */`
|
||||||
|
const expectedContent = `
|
||||||
|
%macro somemacro;
|
||||||
|
%put &sysmacroname;
|
||||||
|
%mend somemacro;
|
||||||
|
/* some comment */
|
||||||
|
/* some comment */ some code;
|
||||||
|
/* some comment */`
|
||||||
|
|
||||||
|
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
|
||||||
|
expect(formattedContent).toEqual(expectedContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should correct mismatched macro name', () => {
|
||||||
|
const content = `
|
||||||
|
%macro somemacro;
|
||||||
|
%put &sysmacroname;
|
||||||
|
%mend someanothermacro;`
|
||||||
|
const expectedContent = `
|
||||||
|
%macro somemacro;
|
||||||
|
%put &sysmacroname;
|
||||||
|
%mend somemacro;`
|
||||||
|
|
||||||
|
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
|
||||||
|
expect(formattedContent).toEqual(expectedContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should correct mismatched macro name with comments', () => {
|
||||||
|
const content = `
|
||||||
|
%macro somemacro;
|
||||||
|
/* some comments */
|
||||||
|
%put &sysmacroname;
|
||||||
|
/* some comments */
|
||||||
|
%mend someanothermacro ;`
|
||||||
|
const expectedContent = `
|
||||||
|
%macro somemacro;
|
||||||
|
/* some comments */
|
||||||
|
%put &sysmacroname;
|
||||||
|
/* some comments */
|
||||||
|
%mend somemacro ;`
|
||||||
|
|
||||||
|
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
|
||||||
|
expect(formattedContent).toEqual(expectedContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use the configured line ending while applying the fix', () => {
|
||||||
|
const content = `%macro somemacro();\r\n%put &sysmacroname;\r\n%mend ;`
|
||||||
|
const expectedContent = `%macro somemacro();\r\n%put &sysmacroname;\r\n%mend somemacro ;`
|
||||||
|
|
||||||
|
const formattedContent = hasMacroNameInMend.fix!(
|
||||||
|
content,
|
||||||
|
new LintConfig({ lineEndings: 'crlf' })
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(formattedContent).toEqual(expectedContent)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
138
src/rules/file/hasMacroNameInMend.ts
Normal file
138
src/rules/file/hasMacroNameInMend.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { Diagnostic } from '../../types/Diagnostic'
|
||||||
|
import { FileLintRule } from '../../types/LintRule'
|
||||||
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
|
import { Severity } from '../../types/Severity'
|
||||||
|
import { getColumnNumber } from '../../utils/getColumnNumber'
|
||||||
|
import { LintConfig } from '../../types'
|
||||||
|
import { LineEndings } from '../../types/LineEndings'
|
||||||
|
import { parseMacros } from '../../utils/parseMacros'
|
||||||
|
|
||||||
|
const name = 'hasMacroNameInMend'
|
||||||
|
const description =
|
||||||
|
'Enforces the presence of the macro name in each %mend statement.'
|
||||||
|
const message = '%mend statement has missing or incorrect macro name'
|
||||||
|
const test = (value: string, config?: LintConfig) => {
|
||||||
|
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
|
||||||
|
const lines: string[] = value ? value.split(lineEnding) : []
|
||||||
|
const macros = parseMacros(value, config)
|
||||||
|
const diagnostics: Diagnostic[] = []
|
||||||
|
macros.forEach((macro) => {
|
||||||
|
if (macro.startLineNumbers.length === 0 && macro.endLineNumber !== null) {
|
||||||
|
const endLine = lines[macro.endLineNumber - 1]
|
||||||
|
diagnostics.push({
|
||||||
|
message: `%mend statement is redundant`,
|
||||||
|
lineNumber: macro.endLineNumber,
|
||||||
|
startColumnNumber: getColumnNumber(endLine, '%mend'),
|
||||||
|
endColumnNumber:
|
||||||
|
getColumnNumber(endLine, '%mend') + macro.termination.length,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
} else if (
|
||||||
|
macro.endLineNumber === null &&
|
||||||
|
macro.startLineNumbers.length !== 0
|
||||||
|
) {
|
||||||
|
diagnostics.push({
|
||||||
|
message: `Missing %mend statement for macro - ${macro.name}`,
|
||||||
|
lineNumber: macro.startLineNumbers![0],
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
} else if (macro.mismatchedMendMacroName) {
|
||||||
|
const endLine = lines[(macro.endLineNumber as number) - 1]
|
||||||
|
diagnostics.push({
|
||||||
|
message: `%mend statement has mismatched macro name, it should be '${
|
||||||
|
macro!.name
|
||||||
|
}'`,
|
||||||
|
lineNumber: macro.endLineNumber as number,
|
||||||
|
startColumnNumber: getColumnNumber(
|
||||||
|
endLine,
|
||||||
|
macro.mismatchedMendMacroName
|
||||||
|
),
|
||||||
|
endColumnNumber:
|
||||||
|
getColumnNumber(endLine, macro.mismatchedMendMacroName) +
|
||||||
|
macro.mismatchedMendMacroName.length -
|
||||||
|
1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
} else if (!macro.hasMacroNameInMend) {
|
||||||
|
const endLine = lines[(macro.endLineNumber as number) - 1]
|
||||||
|
diagnostics.push({
|
||||||
|
message: `%mend statement is missing macro name - ${macro.name}`,
|
||||||
|
lineNumber: macro.endLineNumber as number,
|
||||||
|
startColumnNumber: getColumnNumber(endLine, '%mend'),
|
||||||
|
endColumnNumber: getColumnNumber(endLine, '%mend') + 6,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
const fix = (value: string, config?: LintConfig): string => {
|
||||||
|
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
|
||||||
|
const lines: string[] = value ? value.split(lineEnding) : []
|
||||||
|
const macros = parseMacros(value, config)
|
||||||
|
|
||||||
|
macros.forEach((macro) => {
|
||||||
|
if (macro.startLineNumbers.length === 0 && macro.endLineNumber !== null) {
|
||||||
|
// %mend statement is redundant
|
||||||
|
const endLine = lines[macro.endLineNumber - 1]
|
||||||
|
const startColumnNumber = getColumnNumber(endLine, '%mend')
|
||||||
|
const endColumnNumber =
|
||||||
|
getColumnNumber(endLine, '%mend') + macro.termination.length
|
||||||
|
|
||||||
|
const beforeStatement = endLine.slice(0, startColumnNumber - 1)
|
||||||
|
const afterStatement = endLine.slice(endColumnNumber)
|
||||||
|
lines[macro.endLineNumber - 1] = beforeStatement + afterStatement
|
||||||
|
} else if (
|
||||||
|
macro.endLineNumber === null &&
|
||||||
|
macro.startLineNumbers.length !== 0
|
||||||
|
) {
|
||||||
|
// missing %mend statement
|
||||||
|
} else if (macro.mismatchedMendMacroName) {
|
||||||
|
// mismatched macro name
|
||||||
|
const endLine = lines[(macro.endLineNumber as number) - 1]
|
||||||
|
const startColumnNumber = getColumnNumber(
|
||||||
|
endLine,
|
||||||
|
macro.mismatchedMendMacroName
|
||||||
|
)
|
||||||
|
const endColumnNumber =
|
||||||
|
getColumnNumber(endLine, macro.mismatchedMendMacroName) +
|
||||||
|
macro.mismatchedMendMacroName.length -
|
||||||
|
1
|
||||||
|
|
||||||
|
const beforeMacroName = endLine.slice(0, startColumnNumber - 1)
|
||||||
|
const afterMacroName = endLine.slice(endColumnNumber)
|
||||||
|
|
||||||
|
lines[(macro.endLineNumber as number) - 1] =
|
||||||
|
beforeMacroName + macro.name + afterMacroName
|
||||||
|
} else if (!macro.hasMacroNameInMend) {
|
||||||
|
// %mend statement is missing macro name
|
||||||
|
const endLine = lines[(macro.endLineNumber as number) - 1]
|
||||||
|
const startColumnNumber = getColumnNumber(endLine, '%mend')
|
||||||
|
const endColumnNumber = getColumnNumber(endLine, '%mend') + 4
|
||||||
|
|
||||||
|
const beforeStatement = endLine.slice(0, startColumnNumber - 1)
|
||||||
|
const afterStatement = endLine.slice(endColumnNumber)
|
||||||
|
lines[(macro.endLineNumber as number) - 1] =
|
||||||
|
beforeStatement + `%mend ${macro.name}` + afterStatement
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const formattedText = lines.join(lineEnding)
|
||||||
|
|
||||||
|
return formattedText
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lint rule that checks for the presence of macro name in %mend statement.
|
||||||
|
*/
|
||||||
|
export const hasMacroNameInMend: FileLintRule = {
|
||||||
|
type: LintRuleType.File,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
message,
|
||||||
|
test,
|
||||||
|
fix
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
import { hasMacroParentheses } from './hasMacroParentheses'
|
import { hasMacroParentheses } from './hasMacroParentheses'
|
||||||
|
|
||||||
describe('hasMacroParentheses', () => {
|
describe('hasMacroParentheses', () => {
|
||||||
@@ -16,7 +16,6 @@ describe('hasMacroParentheses', () => {
|
|||||||
%macro somemacro;
|
%macro somemacro;
|
||||||
%put &sysmacroname;
|
%put &sysmacroname;
|
||||||
%mend somemacro;`
|
%mend somemacro;`
|
||||||
|
|
||||||
expect(hasMacroParentheses.test(content)).toEqual([
|
expect(hasMacroParentheses.test(content)).toEqual([
|
||||||
{
|
{
|
||||||
message: 'Macro definition missing parentheses',
|
message: 'Macro definition missing parentheses',
|
||||||
@@ -28,7 +27,7 @@ describe('hasMacroParentheses', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return an array with a single diagnostics when macro defined without name', () => {
|
it('should return an array with a single diagnostic when macro defined without name', () => {
|
||||||
const content = `
|
const content = `
|
||||||
%macro ();
|
%macro ();
|
||||||
%put &sysmacroname;
|
%put &sysmacroname;
|
||||||
@@ -45,7 +44,22 @@ describe('hasMacroParentheses', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return an array with a single diagnostics when macro defined without name and parentheses', () => {
|
it('should return an array with a single diagnostic when macro defined without name ( single line code )', () => {
|
||||||
|
const content = `
|
||||||
|
%macro (); %put &sysmacroname; %mend;`
|
||||||
|
|
||||||
|
expect(hasMacroParentheses.test(content)).toEqual([
|
||||||
|
{
|
||||||
|
message: 'Macro definition missing name',
|
||||||
|
lineNumber: 2,
|
||||||
|
startColumnNumber: 3,
|
||||||
|
endColumnNumber: 12,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a single diagnostic when macro defined without name and parentheses', () => {
|
||||||
const content = `
|
const content = `
|
||||||
%macro ;
|
%macro ;
|
||||||
%put &sysmacroname;
|
%put &sysmacroname;
|
||||||
@@ -65,7 +79,7 @@ describe('hasMacroParentheses', () => {
|
|||||||
it('should return an empty array when the file is undefined', () => {
|
it('should return an empty array when the file is undefined', () => {
|
||||||
const content = undefined
|
const content = undefined
|
||||||
|
|
||||||
expect(hasMacroParentheses.test((content as unknown) as string)).toEqual([])
|
expect(hasMacroParentheses.test(content as unknown as string)).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with extra spaces and comments', () => {
|
describe('with extra spaces and comments', () => {
|
||||||
@@ -125,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
|
|
||||||
}
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
64
src/rules/file/hasMacroParentheses.ts
Normal file
64
src/rules/file/hasMacroParentheses.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { Diagnostic } from '../../types/Diagnostic'
|
||||||
|
import { FileLintRule } from '../../types/LintRule'
|
||||||
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
|
import { Severity } from '../../types/Severity'
|
||||||
|
import { getColumnNumber } from '../../utils/getColumnNumber'
|
||||||
|
import { parseMacros } from '../../utils/parseMacros'
|
||||||
|
import { LintConfig } from '../../types'
|
||||||
|
|
||||||
|
const name = 'hasMacroParentheses'
|
||||||
|
const description = 'Enforces the presence of parentheses in macro definitions.'
|
||||||
|
const message = 'Macro definition missing parentheses'
|
||||||
|
const test = (value: string, config?: LintConfig) => {
|
||||||
|
const diagnostics: Diagnostic[] = []
|
||||||
|
const macros = parseMacros(value, config)
|
||||||
|
macros.forEach((macro) => {
|
||||||
|
if (!macro.name) {
|
||||||
|
diagnostics.push({
|
||||||
|
message: 'Macro definition missing name',
|
||||||
|
lineNumber: macro.startLineNumbers![0],
|
||||||
|
startColumnNumber: getColumnNumber(
|
||||||
|
macro.declarationLines![0],
|
||||||
|
'%macro'
|
||||||
|
),
|
||||||
|
endColumnNumber:
|
||||||
|
getColumnNumber(macro.declarationLines![0], '%macro') +
|
||||||
|
macro.declaration.length,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
} else if (!macro.declarationLines.find((dl) => dl.includes('('))) {
|
||||||
|
const macroNameLineIndex = macro.declarationLines.findIndex((dl) =>
|
||||||
|
dl.includes(macro.name)
|
||||||
|
)
|
||||||
|
diagnostics.push({
|
||||||
|
message,
|
||||||
|
lineNumber: macro.startLineNumbers![macroNameLineIndex],
|
||||||
|
startColumnNumber: getColumnNumber(
|
||||||
|
macro.declarationLines[macroNameLineIndex],
|
||||||
|
macro.name
|
||||||
|
),
|
||||||
|
endColumnNumber:
|
||||||
|
getColumnNumber(
|
||||||
|
macro.declarationLines[macroNameLineIndex],
|
||||||
|
macro.name
|
||||||
|
) +
|
||||||
|
macro.name.length -
|
||||||
|
1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lint rule that enforces the presence of parentheses in macro definitions..
|
||||||
|
*/
|
||||||
|
export const hasMacroParentheses: FileLintRule = {
|
||||||
|
type: LintRuleType.File,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
message,
|
||||||
|
test
|
||||||
|
}
|
||||||
6
src/rules/file/index.ts
Normal file
6
src/rules/file/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export { hasDoxygenHeader } from './hasDoxygenHeader'
|
||||||
|
export { hasMacroNameInMend } from './hasMacroNameInMend'
|
||||||
|
export { hasMacroParentheses } from './hasMacroParentheses'
|
||||||
|
export { lineEndings } from './lineEndings'
|
||||||
|
export { noNestedMacros } from './noNestedMacros'
|
||||||
|
export { strictMacroDefinition } from './strictMacroDefinition'
|
||||||
141
src/rules/file/lineEndings.spec.ts
Normal file
141
src/rules/file/lineEndings.spec.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { LintConfig, Severity } from '../../types'
|
||||||
|
import { LineEndings } from '../../types/LineEndings'
|
||||||
|
import { lineEndings } from './lineEndings'
|
||||||
|
|
||||||
|
describe('lineEndings - test', () => {
|
||||||
|
it('should return an empty array when the text contains the configured line endings', () => {
|
||||||
|
const text = "%put 'hello';\n%put 'world';\n"
|
||||||
|
const config = new LintConfig({ lineEndings: LineEndings.LF })
|
||||||
|
expect(lineEndings.test(text, config)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a single diagnostic when a line is terminated with a CRLF ending', () => {
|
||||||
|
const text = "%put 'hello';\n%put 'world';\r\n"
|
||||||
|
const config = new LintConfig({ lineEndings: LineEndings.LF })
|
||||||
|
expect(lineEndings.test(text, config)).toEqual([
|
||||||
|
{
|
||||||
|
message: 'Incorrect line ending - CRLF instead of LF',
|
||||||
|
lineNumber: 2,
|
||||||
|
startColumnNumber: 13,
|
||||||
|
endColumnNumber: 14,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a single diagnostic when a line is terminated with an LF ending', () => {
|
||||||
|
const text = "%put 'hello';\n%put 'world';\r\n"
|
||||||
|
const config = new LintConfig({ lineEndings: LineEndings.CRLF })
|
||||||
|
expect(lineEndings.test(text, config)).toEqual([
|
||||||
|
{
|
||||||
|
message: 'Incorrect line ending - LF instead of CRLF',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 13,
|
||||||
|
endColumnNumber: 14,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a diagnostic for each line terminated with an LF ending', () => {
|
||||||
|
const text = "%put 'hello';\n%put 'test';\r\n%put 'world';\n"
|
||||||
|
const config = new LintConfig({ lineEndings: LineEndings.CRLF })
|
||||||
|
expect(lineEndings.test(text, config)).toContainEqual({
|
||||||
|
message: 'Incorrect line ending - LF instead of CRLF',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 13,
|
||||||
|
endColumnNumber: 14,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(lineEndings.test(text, config)).toContainEqual({
|
||||||
|
message: 'Incorrect line ending - LF instead of CRLF',
|
||||||
|
lineNumber: 3,
|
||||||
|
startColumnNumber: 13,
|
||||||
|
endColumnNumber: 14,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a diagnostic for each line terminated with a CRLF ending', () => {
|
||||||
|
const text = "%put 'hello';\r\n%put 'test';\n%put 'world';\r\n"
|
||||||
|
const config = new LintConfig({ lineEndings: LineEndings.LF })
|
||||||
|
expect(lineEndings.test(text, config)).toContainEqual({
|
||||||
|
message: 'Incorrect line ending - CRLF instead of LF',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 13,
|
||||||
|
endColumnNumber: 14,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(lineEndings.test(text, config)).toContainEqual({
|
||||||
|
message: 'Incorrect line ending - CRLF instead of LF',
|
||||||
|
lineNumber: 3,
|
||||||
|
startColumnNumber: 13,
|
||||||
|
endColumnNumber: 14,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a diagnostic for lines terminated with a CRLF ending', () => {
|
||||||
|
const text =
|
||||||
|
"%put 'hello';\r\n%put 'test';\r\n%put 'world';\n%put 'test2';\n%put 'world2';\r\n"
|
||||||
|
const config = new LintConfig({ lineEndings: LineEndings.LF })
|
||||||
|
expect(lineEndings.test(text, config)).toContainEqual({
|
||||||
|
message: 'Incorrect line ending - CRLF instead of LF',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 13,
|
||||||
|
endColumnNumber: 14,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(lineEndings.test(text, config)).toContainEqual({
|
||||||
|
message: 'Incorrect line ending - CRLF instead of LF',
|
||||||
|
lineNumber: 2,
|
||||||
|
startColumnNumber: 12,
|
||||||
|
endColumnNumber: 13,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(lineEndings.test(text, config)).toContainEqual({
|
||||||
|
message: 'Incorrect line ending - CRLF instead of LF',
|
||||||
|
lineNumber: 5,
|
||||||
|
startColumnNumber: 14,
|
||||||
|
endColumnNumber: 15,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('lineEndings - fix', () => {
|
||||||
|
it('should transform line endings to LF', () => {
|
||||||
|
const text =
|
||||||
|
"%put 'hello';\r\n%put 'test';\r\n%put 'world';\n%put 'test2';\n%put 'world2';\r\n"
|
||||||
|
const config = new LintConfig({ lineEndings: LineEndings.LF })
|
||||||
|
|
||||||
|
const formattedText = lineEndings.fix!(text, config)
|
||||||
|
|
||||||
|
expect(formattedText).toEqual(
|
||||||
|
"%put 'hello';\n%put 'test';\n%put 'world';\n%put 'test2';\n%put 'world2';\n"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should transform line endings to CRLF', () => {
|
||||||
|
const text =
|
||||||
|
"%put 'hello';\r\n%put 'test';\r\n%put 'world';\n%put 'test2';\n%put 'world2';\r\n"
|
||||||
|
const config = new LintConfig({ lineEndings: LineEndings.CRLF })
|
||||||
|
|
||||||
|
const formattedText = lineEndings.fix!(text, config)
|
||||||
|
|
||||||
|
expect(formattedText).toEqual(
|
||||||
|
"%put 'hello';\r\n%put 'test';\r\n%put 'world';\r\n%put 'test2';\r\n%put 'world2';\r\n"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use LF line endings by default', () => {
|
||||||
|
const text =
|
||||||
|
"%put 'hello';\r\n%put 'test';\r\n%put 'world';\n%put 'test2';\n%put 'world2';\r\n"
|
||||||
|
|
||||||
|
const formattedText = lineEndings.fix!(text)
|
||||||
|
|
||||||
|
expect(formattedText).toEqual(
|
||||||
|
"%put 'hello';\n%put 'test';\n%put 'world';\n%put 'test2';\n%put 'world2';\n"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
83
src/rules/file/lineEndings.ts
Normal file
83
src/rules/file/lineEndings.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { Diagnostic, LintConfig } from '../../types'
|
||||||
|
import { LineEndings } from '../../types/LineEndings'
|
||||||
|
import { FileLintRule } from '../../types/LintRule'
|
||||||
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
|
import { Severity } from '../../types/Severity'
|
||||||
|
|
||||||
|
const name = 'lineEndings'
|
||||||
|
const description = 'Ensures line endings conform to the configured type.'
|
||||||
|
const message = 'Incorrect line ending - {actual} instead of {expected}'
|
||||||
|
const test = (value: string, config?: LintConfig) => {
|
||||||
|
const lineEndingConfig = config?.lineEndings || LineEndings.LF
|
||||||
|
const expectedLineEnding =
|
||||||
|
lineEndingConfig === LineEndings.LF ? '{lf}' : '{crlf}'
|
||||||
|
const incorrectLineEnding = expectedLineEnding === '{lf}' ? '{crlf}' : '{lf}'
|
||||||
|
|
||||||
|
const lines = value
|
||||||
|
.replace(/\r\n/g, '{crlf}')
|
||||||
|
.replace(/\n/g, '{lf}')
|
||||||
|
.split(new RegExp(`(?<=${expectedLineEnding})`))
|
||||||
|
const diagnostics: Diagnostic[] = []
|
||||||
|
|
||||||
|
let indexOffset = 0
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
if (line.endsWith(incorrectLineEnding)) {
|
||||||
|
diagnostics.push({
|
||||||
|
message: message
|
||||||
|
.replace('{expected}', expectedLineEnding === '{lf}' ? 'LF' : 'CRLF')
|
||||||
|
.replace('{actual}', incorrectLineEnding === '{lf}' ? 'LF' : 'CRLF'),
|
||||||
|
lineNumber: index + 1 + indexOffset,
|
||||||
|
startColumnNumber: line.indexOf(incorrectLineEnding),
|
||||||
|
endColumnNumber: line.indexOf(incorrectLineEnding) + 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const splitLine = line.split(new RegExp(`(?<=${incorrectLineEnding})`))
|
||||||
|
if (splitLine.length > 1) {
|
||||||
|
indexOffset += splitLine.length - 1
|
||||||
|
}
|
||||||
|
splitLine.forEach((l, i) => {
|
||||||
|
if (l.endsWith(incorrectLineEnding)) {
|
||||||
|
diagnostics.push({
|
||||||
|
message: message
|
||||||
|
.replace(
|
||||||
|
'{expected}',
|
||||||
|
expectedLineEnding === '{lf}' ? 'LF' : 'CRLF'
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
'{actual}',
|
||||||
|
incorrectLineEnding === '{lf}' ? 'LF' : 'CRLF'
|
||||||
|
),
|
||||||
|
lineNumber: index + i + 1,
|
||||||
|
startColumnNumber: l.indexOf(incorrectLineEnding),
|
||||||
|
endColumnNumber: l.indexOf(incorrectLineEnding) + 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
const fix = (value: string, config?: LintConfig): string => {
|
||||||
|
const lineEndingConfig = config?.lineEndings || LineEndings.LF
|
||||||
|
|
||||||
|
return value
|
||||||
|
.replace(/\r\n/g, '{crlf}')
|
||||||
|
.replace(/\n/g, '{lf}')
|
||||||
|
.replace(/{crlf}/g, lineEndingConfig === LineEndings.LF ? '\n' : '\r\n')
|
||||||
|
.replace(/{lf}/g, lineEndingConfig === LineEndings.LF ? '\n' : '\r\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lint rule that checks if line endings in a file match the configured type.
|
||||||
|
*/
|
||||||
|
export const lineEndings: FileLintRule = {
|
||||||
|
type: LintRuleType.File,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
message,
|
||||||
|
test,
|
||||||
|
fix
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Severity } from '../types/Severity'
|
import { LintConfig } from '../../types'
|
||||||
|
import { Severity } from '../../types/Severity'
|
||||||
import { noNestedMacros } from './noNestedMacros'
|
import { noNestedMacros } from './noNestedMacros'
|
||||||
|
|
||||||
describe('noNestedMacros', () => {
|
describe('noNestedMacros', () => {
|
||||||
@@ -29,13 +30,13 @@ describe('noNestedMacros', () => {
|
|||||||
message: "Macro definition for 'inner' present in macro 'outer'",
|
message: "Macro definition for 'inner' present in macro 'outer'",
|
||||||
lineNumber: 4,
|
lineNumber: 4,
|
||||||
startColumnNumber: 7,
|
startColumnNumber: 7,
|
||||||
endColumnNumber: 20,
|
endColumnNumber: 21,
|
||||||
severity: Severity.Warning
|
severity: Severity.Warning
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return an array with a single diagnostic when nested macros are defined at 2 levels', () => {
|
it('should return an array with two diagnostics when nested macros are defined at 2 levels', () => {
|
||||||
const content = `
|
const content = `
|
||||||
%macro outer();
|
%macro outer();
|
||||||
/* any amount of arbitrary code */
|
/* any amount of arbitrary code */
|
||||||
@@ -52,27 +53,44 @@ describe('noNestedMacros', () => {
|
|||||||
|
|
||||||
%outer()`
|
%outer()`
|
||||||
|
|
||||||
expect(noNestedMacros.test(content)).toEqual([
|
expect(noNestedMacros.test(content)).toContainEqual({
|
||||||
{
|
message: "Macro definition for 'inner' present in macro 'outer'",
|
||||||
message: "Macro definition for 'inner' present in macro 'outer'",
|
lineNumber: 4,
|
||||||
lineNumber: 4,
|
startColumnNumber: 7,
|
||||||
startColumnNumber: 7,
|
endColumnNumber: 21,
|
||||||
endColumnNumber: 20,
|
severity: Severity.Warning
|
||||||
severity: Severity.Warning
|
})
|
||||||
},
|
expect(noNestedMacros.test(content)).toContainEqual({
|
||||||
{
|
message: "Macro definition for 'inner2' present in macro 'inner'",
|
||||||
message: "Macro definition for 'inner2' present in macro 'inner'",
|
lineNumber: 7,
|
||||||
lineNumber: 7,
|
startColumnNumber: 17,
|
||||||
startColumnNumber: 17,
|
endColumnNumber: 32,
|
||||||
endColumnNumber: 31,
|
severity: Severity.Warning
|
||||||
severity: Severity.Warning
|
})
|
||||||
}
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return an empty array when the file is undefined', () => {
|
it('should return an empty array when the file is undefined', () => {
|
||||||
const content = undefined
|
const content = undefined
|
||||||
|
|
||||||
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', () => {
|
||||||
|
const content = `%macro outer();\r\n%macro inner;\r\n%mend inner;\r\n%mend outer;`
|
||||||
|
|
||||||
|
const diagnostics = noNestedMacros.test(
|
||||||
|
content,
|
||||||
|
new LintConfig({ lineEndings: 'crlf' })
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(diagnostics).toEqual([
|
||||||
|
{
|
||||||
|
message: "Macro definition for 'inner' present in macro 'outer'",
|
||||||
|
lineNumber: 2,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 13,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
52
src/rules/file/noNestedMacros.ts
Normal file
52
src/rules/file/noNestedMacros.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Diagnostic } from '../../types/Diagnostic'
|
||||||
|
import { FileLintRule } from '../../types/LintRule'
|
||||||
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
|
import { Severity } from '../../types/Severity'
|
||||||
|
import { getColumnNumber } from '../../utils/getColumnNumber'
|
||||||
|
import { parseMacros } from '../../utils/parseMacros'
|
||||||
|
import { LintConfig } from '../../types'
|
||||||
|
import { LineEndings } from '../../types/LineEndings'
|
||||||
|
|
||||||
|
const name = 'noNestedMacros'
|
||||||
|
const description = 'Enfoces the absence of nested macro definitions.'
|
||||||
|
const message = `Macro definition for '{macro}' present in macro '{parent}'`
|
||||||
|
const test = (value: string, config?: LintConfig) => {
|
||||||
|
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
|
||||||
|
const lines: string[] = value ? value.split(lineEnding) : []
|
||||||
|
const diagnostics: Diagnostic[] = []
|
||||||
|
const macros = parseMacros(value, config)
|
||||||
|
macros
|
||||||
|
.filter((m) => !!m.parentMacro)
|
||||||
|
.forEach((macro) => {
|
||||||
|
diagnostics.push({
|
||||||
|
message: message
|
||||||
|
.replace('{macro}', macro.name)
|
||||||
|
.replace('{parent}', macro.parentMacro),
|
||||||
|
lineNumber: macro.startLineNumbers![0] as number,
|
||||||
|
startColumnNumber: getColumnNumber(
|
||||||
|
lines[(macro.startLineNumbers![0] as number) - 1],
|
||||||
|
'%macro'
|
||||||
|
),
|
||||||
|
endColumnNumber:
|
||||||
|
getColumnNumber(
|
||||||
|
lines[(macro.startLineNumbers![0] as number) - 1],
|
||||||
|
'%macro'
|
||||||
|
) +
|
||||||
|
lines[(macro.startLineNumbers![0] as number) - 1].trim().length -
|
||||||
|
1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lint rule that checks for the absence of nested macro definitions.
|
||||||
|
*/
|
||||||
|
export const noNestedMacros: FileLintRule = {
|
||||||
|
type: LintRuleType.File,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
message,
|
||||||
|
test
|
||||||
|
}
|
||||||
216
src/rules/file/strictMacroDefinition.spec.ts
Normal file
216
src/rules/file/strictMacroDefinition.spec.ts
Normal 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
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
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
169
src/rules/file/strictMacroDefinition.ts
Normal file
169
src/rules/file/strictMacroDefinition.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import { Diagnostic } from '../types/Diagnostic'
|
|
||||||
import { FileLintRule } from '../types/LintRule'
|
|
||||||
import { LintRuleType } from '../types/LintRuleType'
|
|
||||||
import { Severity } from '../types/Severity'
|
|
||||||
import { trimComments } from '../utils/trimComments'
|
|
||||||
import { getColumnNumber } from '../utils/getColumnNumber'
|
|
||||||
|
|
||||||
const name = 'hasMacroNameInMend'
|
|
||||||
const description =
|
|
||||||
'Enforces the presence of the macro name in each %mend statement.'
|
|
||||||
const message = '%mend statement has missing or incorrect macro name'
|
|
||||||
const test = (value: string) => {
|
|
||||||
const diagnostics: Diagnostic[] = []
|
|
||||||
|
|
||||||
const lines: string[] = value ? value.split('\n') : []
|
|
||||||
|
|
||||||
const declaredMacros: { name: string; lineNumber: number }[] = []
|
|
||||||
let isCommentStarted = false
|
|
||||||
lines.forEach((line, lineIndex) => {
|
|
||||||
const { statement: trimmedLine, commentStarted } = trimComments(
|
|
||||||
line,
|
|
||||||
isCommentStarted
|
|
||||||
)
|
|
||||||
isCommentStarted = commentStarted
|
|
||||||
const statements: string[] = trimmedLine ? trimmedLine.split(';') : []
|
|
||||||
|
|
||||||
statements.forEach((statement) => {
|
|
||||||
const { statement: trimmedStatement, commentStarted } = trimComments(
|
|
||||||
statement,
|
|
||||||
isCommentStarted
|
|
||||||
)
|
|
||||||
isCommentStarted = commentStarted
|
|
||||||
|
|
||||||
if (trimmedStatement.startsWith('%macro ')) {
|
|
||||||
const macroName = trimmedStatement
|
|
||||||
.slice(7, trimmedStatement.length)
|
|
||||||
.trim()
|
|
||||||
.split('(')[0]
|
|
||||||
if (macroName)
|
|
||||||
declaredMacros.push({
|
|
||||||
name: macroName,
|
|
||||||
lineNumber: lineIndex + 1
|
|
||||||
})
|
|
||||||
} else if (trimmedStatement.startsWith('%mend')) {
|
|
||||||
const declaredMacro = declaredMacros.pop()
|
|
||||||
const macroName = trimmedStatement
|
|
||||||
.split(' ')
|
|
||||||
.filter((s: string) => !!s)[1]
|
|
||||||
|
|
||||||
if (!declaredMacro) {
|
|
||||||
diagnostics.push({
|
|
||||||
message: `%mend statement is redundant`,
|
|
||||||
lineNumber: lineIndex + 1,
|
|
||||||
startColumnNumber: getColumnNumber(line, '%mend'),
|
|
||||||
endColumnNumber:
|
|
||||||
getColumnNumber(line, '%mend') + trimmedStatement.length,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
} else if (!macroName) {
|
|
||||||
diagnostics.push({
|
|
||||||
message: `%mend statement is missing macro name - ${
|
|
||||||
declaredMacro!.name
|
|
||||||
}`,
|
|
||||||
lineNumber: lineIndex + 1,
|
|
||||||
startColumnNumber: getColumnNumber(line, '%mend'),
|
|
||||||
endColumnNumber: getColumnNumber(line, '%mend') + 6,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
} else if (macroName !== declaredMacro!.name) {
|
|
||||||
diagnostics.push({
|
|
||||||
message: `%mend statement has mismatched macro name, it should be '${
|
|
||||||
declaredMacro!.name
|
|
||||||
}'`,
|
|
||||||
lineNumber: lineIndex + 1,
|
|
||||||
startColumnNumber: getColumnNumber(line, macroName),
|
|
||||||
endColumnNumber:
|
|
||||||
getColumnNumber(line, macroName) + macroName.length - 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
declaredMacros.forEach((declaredMacro) => {
|
|
||||||
diagnostics.push({
|
|
||||||
message: `Missing %mend statement for macro - ${declaredMacro.name}`,
|
|
||||||
lineNumber: declaredMacro.lineNumber,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lint rule that checks for the presence of macro name in %mend statement.
|
|
||||||
*/
|
|
||||||
export const hasMacroNameInMend: FileLintRule = {
|
|
||||||
type: LintRuleType.File,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
message,
|
|
||||||
test
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import { Diagnostic } from '../types/Diagnostic'
|
|
||||||
import { FileLintRule } from '../types/LintRule'
|
|
||||||
import { LintRuleType } from '../types/LintRuleType'
|
|
||||||
import { Severity } from '../types/Severity'
|
|
||||||
import { trimComments } from '../utils/trimComments'
|
|
||||||
import { getColumnNumber } from '../utils/getColumnNumber'
|
|
||||||
|
|
||||||
const name = 'hasMacroParentheses'
|
|
||||||
const description = 'Enforces the presence of parentheses in macro definitions.'
|
|
||||||
const message = 'Macro definition missing parentheses'
|
|
||||||
const test = (value: string) => {
|
|
||||||
const diagnostics: Diagnostic[] = []
|
|
||||||
|
|
||||||
const lines: string[] = value ? value.split('\n') : []
|
|
||||||
let isCommentStarted = false
|
|
||||||
lines.forEach((line, lineIndex) => {
|
|
||||||
const { statement: trimmedLine, commentStarted } = trimComments(
|
|
||||||
line,
|
|
||||||
isCommentStarted
|
|
||||||
)
|
|
||||||
isCommentStarted = commentStarted
|
|
||||||
const statements: string[] = trimmedLine ? trimmedLine.split(';') : []
|
|
||||||
|
|
||||||
statements.forEach((statement) => {
|
|
||||||
const { statement: trimmedStatement, commentStarted } = trimComments(
|
|
||||||
statement,
|
|
||||||
isCommentStarted
|
|
||||||
)
|
|
||||||
isCommentStarted = commentStarted
|
|
||||||
|
|
||||||
if (trimmedStatement.startsWith('%macro')) {
|
|
||||||
const macroNameDefinition = trimmedStatement
|
|
||||||
.slice(7, trimmedStatement.length)
|
|
||||||
.trim()
|
|
||||||
|
|
||||||
const macroNameDefinitionParts = macroNameDefinition.split('(')
|
|
||||||
const macroName = macroNameDefinitionParts[0]
|
|
||||||
|
|
||||||
if (!macroName)
|
|
||||||
diagnostics.push({
|
|
||||||
message: 'Macro definition missing name',
|
|
||||||
lineNumber: lineIndex + 1,
|
|
||||||
startColumnNumber: getColumnNumber(line, '%macro'),
|
|
||||||
endColumnNumber:
|
|
||||||
getColumnNumber(line, '%macro') + trimmedStatement.length,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
else if (macroNameDefinitionParts.length === 1)
|
|
||||||
diagnostics.push({
|
|
||||||
message,
|
|
||||||
lineNumber: lineIndex + 1,
|
|
||||||
startColumnNumber: getColumnNumber(line, macroNameDefinition),
|
|
||||||
endColumnNumber:
|
|
||||||
getColumnNumber(line, macroNameDefinition) +
|
|
||||||
macroNameDefinition.length -
|
|
||||||
1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
else if (macroName !== macroName.trim())
|
|
||||||
diagnostics.push({
|
|
||||||
message: 'Macro definition contains space(s)',
|
|
||||||
lineNumber: lineIndex + 1,
|
|
||||||
startColumnNumber: getColumnNumber(line, macroNameDefinition),
|
|
||||||
endColumnNumber:
|
|
||||||
getColumnNumber(line, macroNameDefinition) +
|
|
||||||
macroNameDefinition.length -
|
|
||||||
1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lint rule that enforces the presence of parentheses in macro definitions..
|
|
||||||
*/
|
|
||||||
export const hasMacroParentheses: FileLintRule = {
|
|
||||||
type: LintRuleType.File,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
message,
|
|
||||||
test
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LintConfig, Severity } from '../types'
|
import { LintConfig, Severity } from '../../types'
|
||||||
import { indentationMultiple } from './indentationMultiple'
|
import { indentationMultiple } from './indentationMultiple'
|
||||||
|
|
||||||
describe('indentationMultiple', () => {
|
describe('indentationMultiple', () => {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { LintConfig } from '../types'
|
import { LintConfig } from '../../types'
|
||||||
import { LineLintRule } from '../types/LintRule'
|
import { LineLintRule } from '../../types/LintRule'
|
||||||
import { LintRuleType } from '../types/LintRuleType'
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
|
|
||||||
const name = 'indentationMultiple'
|
const name = 'indentationMultiple'
|
||||||
const description = 'Ensure indentation by a multiple of the configured number.'
|
const description = 'Ensure indentation by a multiple of the configured number.'
|
||||||
5
src/rules/line/index.ts
Normal file
5
src/rules/line/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export { indentationMultiple } from './indentationMultiple'
|
||||||
|
export { maxLineLength } from './maxLineLength'
|
||||||
|
export { noEncodedPasswords } from './noEncodedPasswords'
|
||||||
|
export { noTabIndentation } from './noTabIndentation'
|
||||||
|
export { noTrailingSpaces } from './noTrailingSpaces'
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LintConfig, Severity } from '../types'
|
import { LintConfig, Severity } from '../../types'
|
||||||
import { maxLineLength } from './maxLineLength'
|
import { maxLineLength } from './maxLineLength'
|
||||||
|
|
||||||
describe('maxLineLength', () => {
|
describe('maxLineLength', () => {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { LintConfig } from '../types'
|
import { LintConfig } from '../../types'
|
||||||
import { LineLintRule } from '../types/LintRule'
|
import { LineLintRule } from '../../types/LintRule'
|
||||||
import { LintRuleType } from '../types/LintRuleType'
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
|
|
||||||
const name = 'maxLineLength'
|
const name = 'maxLineLength'
|
||||||
const description = 'Restrict lines to the specified length.'
|
const description = 'Restrict lines to the specified length.'
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
import { noEncodedPasswords } from './noEncodedPasswords'
|
import { noEncodedPasswords } from './noEncodedPasswords'
|
||||||
|
|
||||||
describe('noEncodedPasswords', () => {
|
describe('noEncodedPasswords', () => {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LineLintRule } from '../types/LintRule'
|
import { LineLintRule } from '../../types/LintRule'
|
||||||
import { LintRuleType } from '../types/LintRuleType'
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
|
|
||||||
const name = 'noEncodedPasswords'
|
const name = 'noEncodedPasswords'
|
||||||
const description = 'Disallow encoded passwords in SAS code.'
|
const description = 'Disallow encoded passwords in SAS code.'
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
import { noTabIndentation } from './noTabIndentation'
|
import { noTabIndentation } from './noTabIndentation'
|
||||||
|
|
||||||
describe('noTabs', () => {
|
describe('noTabs', () => {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LineLintRule } from '../types/LintRule'
|
import { LineLintRule } from '../../types/LintRule'
|
||||||
import { LintRuleType } from '../types/LintRuleType'
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
|
|
||||||
const name = 'noTabs'
|
const name = 'noTabs'
|
||||||
const description = 'Disallow indenting with tabs.'
|
const description = 'Disallow indenting with tabs.'
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
import { noTrailingSpaces } from './noTrailingSpaces'
|
import { noTrailingSpaces } from './noTrailingSpaces'
|
||||||
|
|
||||||
describe('noTrailingSpaces', () => {
|
describe('noTrailingSpaces', () => {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LineLintRule } from '../types/LintRule'
|
import { LineLintRule } from '../../types/LintRule'
|
||||||
import { LintRuleType } from '../types/LintRuleType'
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
|
|
||||||
const name = 'noTrailingSpaces'
|
const name = 'noTrailingSpaces'
|
||||||
const description = 'Disallow trailing spaces on lines.'
|
const description = 'Disallow trailing spaces on lines.'
|
||||||
@@ -17,6 +17,7 @@ const test = (value: string, lineNumber: number) =>
|
|||||||
severity: Severity.Warning
|
severity: Severity.Warning
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
const fix = (value: string) => value.trimEnd()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lint rule that checks for the presence of trailing space(s) in a given line of text.
|
* Lint rule that checks for the presence of trailing space(s) in a given line of text.
|
||||||
@@ -26,5 +27,6 @@ export const noTrailingSpaces: LineLintRule = {
|
|||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
message,
|
message,
|
||||||
test
|
test,
|
||||||
|
fix
|
||||||
}
|
}
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { Diagnostic } from '../types/Diagnostic'
|
|
||||||
import { FileLintRule } from '../types/LintRule'
|
|
||||||
import { LintRuleType } from '../types/LintRuleType'
|
|
||||||
import { Severity } from '../types/Severity'
|
|
||||||
import { trimComments } from '../utils/trimComments'
|
|
||||||
import { getColumnNumber } from '../utils/getColumnNumber'
|
|
||||||
|
|
||||||
const name = 'noNestedMacros'
|
|
||||||
const description = 'Enfoces the absence of nested macro definitions.'
|
|
||||||
const message = `Macro definition for '{macro}' present in macro '{parent}'`
|
|
||||||
const test = (value: string) => {
|
|
||||||
const diagnostics: Diagnostic[] = []
|
|
||||||
const declaredMacros: string[] = []
|
|
||||||
|
|
||||||
const lines: string[] = value ? value.split('\n') : []
|
|
||||||
let isCommentStarted = false
|
|
||||||
lines.forEach((line, lineIndex) => {
|
|
||||||
const { statement: trimmedLine, commentStarted } = trimComments(
|
|
||||||
line,
|
|
||||||
isCommentStarted
|
|
||||||
)
|
|
||||||
isCommentStarted = commentStarted
|
|
||||||
const statements: string[] = trimmedLine ? trimmedLine.split(';') : []
|
|
||||||
|
|
||||||
statements.forEach((statement) => {
|
|
||||||
const { statement: trimmedStatement, commentStarted } = trimComments(
|
|
||||||
statement,
|
|
||||||
isCommentStarted
|
|
||||||
)
|
|
||||||
isCommentStarted = commentStarted
|
|
||||||
|
|
||||||
if (trimmedStatement.startsWith('%macro ')) {
|
|
||||||
const macroName = trimmedStatement
|
|
||||||
.slice(7, trimmedStatement.length)
|
|
||||||
.trim()
|
|
||||||
.split('(')[0]
|
|
||||||
if (declaredMacros.length) {
|
|
||||||
const parentMacro = declaredMacros.slice(-1).pop()
|
|
||||||
diagnostics.push({
|
|
||||||
message: message
|
|
||||||
.replace('{macro}', macroName)
|
|
||||||
.replace('{parent}', parentMacro!),
|
|
||||||
lineNumber: lineIndex + 1,
|
|
||||||
startColumnNumber: getColumnNumber(line, '%macro'),
|
|
||||||
endColumnNumber:
|
|
||||||
getColumnNumber(line, '%macro') + trimmedStatement.length - 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
}
|
|
||||||
declaredMacros.push(macroName)
|
|
||||||
} else if (trimmedStatement.startsWith('%mend')) {
|
|
||||||
declaredMacros.pop()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lint rule that checks for the absence of nested macro definitions.
|
|
||||||
*/
|
|
||||||
export const noNestedMacros: FileLintRule = {
|
|
||||||
type: LintRuleType.File,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
message,
|
|
||||||
test
|
|
||||||
}
|
|
||||||
2
src/rules/path/index.ts
Normal file
2
src/rules/path/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { lowerCaseFileNames } from './lowerCaseFileNames'
|
||||||
|
export { noSpacesInFileNames } from './noSpacesInFileNames'
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
import { lowerCaseFileNames } from './lowerCaseFileNames'
|
import { lowerCaseFileNames } from './lowerCaseFileNames'
|
||||||
|
|
||||||
describe('lowerCaseFileNames', () => {
|
describe('lowerCaseFileNames', () => {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PathLintRule } from '../types/LintRule'
|
import { PathLintRule } from '../../types/LintRule'
|
||||||
import { LintRuleType } from '../types/LintRuleType'
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
const name = 'lowerCaseFileNames'
|
const name = 'lowerCaseFileNames'
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
import { noSpacesInFileNames } from './noSpacesInFileNames'
|
import { noSpacesInFileNames } from './noSpacesInFileNames'
|
||||||
|
|
||||||
describe('noSpacesInFileNames', () => {
|
describe('noSpacesInFileNames', () => {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PathLintRule } from '../types/LintRule'
|
import { PathLintRule } from '../../types/LintRule'
|
||||||
import { LintRuleType } from '../types/LintRuleType'
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
const name = 'noSpacesInFileNames'
|
const name = 'noSpacesInFileNames'
|
||||||
10
src/types/FormatResult.ts
Normal file
10
src/types/FormatResult.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Diagnostic } from './Diagnostic'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the result of a format operation on a file, folder or project.
|
||||||
|
*/
|
||||||
|
export interface FormatResult {
|
||||||
|
updatedFilePaths: string[]
|
||||||
|
fixedDiagnosticsCount: number
|
||||||
|
unfixedDiagnostics: Map<string, Diagnostic[]> | Diagnostic[]
|
||||||
|
}
|
||||||
4
src/types/LineEndings.ts
Normal file
4
src/types/LineEndings.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum LineEndings {
|
||||||
|
LF = 'lf',
|
||||||
|
CRLF = 'crlf'
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LineEndings } from './LineEndings'
|
||||||
import { LintConfig } from './LintConfig'
|
import { LintConfig } from './LintConfig'
|
||||||
import { LintRuleType } from './LintRuleType'
|
import { LintRuleType } from './LintRuleType'
|
||||||
|
|
||||||
@@ -108,6 +109,33 @@ describe('LintConfig', () => {
|
|||||||
expect(config.indentationMultiple).toEqual(0)
|
expect(config.indentationMultiple).toEqual(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should create an instance with the line endings set to LF', () => {
|
||||||
|
const config = new LintConfig({ lineEndings: 'lf' })
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.lineEndings).toEqual(LineEndings.LF)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create an instance with the line endings set to CRLF', () => {
|
||||||
|
const config = new LintConfig({ lineEndings: 'crlf' })
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.lineEndings).toEqual(LineEndings.CRLF)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create an instance with the line endings set to LF by default', () => {
|
||||||
|
const config = new LintConfig({})
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.lineEndings).toEqual(LineEndings.LF)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw an error with an invalid value for line endings', () => {
|
||||||
|
expect(() => new LintConfig({ lineEndings: 'test' })).toThrowError(
|
||||||
|
`Invalid value for lineEndings: can be ${LineEndings.LF} or ${LineEndings.CRLF}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('should create an instance with all flags set', () => {
|
it('should create an instance with all flags set', () => {
|
||||||
const config = new LintConfig({
|
const config = new LintConfig({
|
||||||
noTrailingSpaces: true,
|
noTrailingSpaces: true,
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
import { hasDoxygenHeader } from '../rules/hasDoxygenHeader'
|
import {
|
||||||
import { indentationMultiple } from '../rules/indentationMultiple'
|
hasDoxygenHeader,
|
||||||
import { lowerCaseFileNames } from '../rules/lowerCaseFileNames'
|
hasMacroNameInMend,
|
||||||
import { maxLineLength } from '../rules/maxLineLength'
|
noNestedMacros,
|
||||||
import { noEncodedPasswords } from '../rules/noEncodedPasswords'
|
hasMacroParentheses,
|
||||||
import { noSpacesInFileNames } from '../rules/noSpacesInFileNames'
|
lineEndings,
|
||||||
import { noTabIndentation } from '../rules/noTabIndentation'
|
strictMacroDefinition
|
||||||
import { noTrailingSpaces } from '../rules/noTrailingSpaces'
|
} from '../rules/file'
|
||||||
import { hasMacroNameInMend } from '../rules/hasMacroNameInMend'
|
import {
|
||||||
import { noNestedMacros } from '../rules/noNestedMacros'
|
indentationMultiple,
|
||||||
import { hasMacroParentheses } from '../rules/hasMacroParentheses'
|
maxLineLength,
|
||||||
|
noEncodedPasswords,
|
||||||
|
noTabIndentation,
|
||||||
|
noTrailingSpaces
|
||||||
|
} from '../rules/line'
|
||||||
|
import { lowerCaseFileNames, noSpacesInFileNames } from '../rules/path'
|
||||||
|
import { LineEndings } from './LineEndings'
|
||||||
import { FileLintRule, LineLintRule, PathLintRule } from './LintRule'
|
import { FileLintRule, LineLintRule, PathLintRule } from './LintRule'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,6 +30,7 @@ export class LintConfig {
|
|||||||
readonly pathLintRules: PathLintRule[] = []
|
readonly pathLintRules: PathLintRule[] = []
|
||||||
readonly maxLineLength: number = 80
|
readonly maxLineLength: number = 80
|
||||||
readonly indentationMultiple: number = 2
|
readonly indentationMultiple: number = 2
|
||||||
|
readonly lineEndings: LineEndings = LineEndings.LF
|
||||||
|
|
||||||
constructor(json?: any) {
|
constructor(json?: any) {
|
||||||
if (json?.noTrailingSpaces) {
|
if (json?.noTrailingSpaces) {
|
||||||
@@ -43,6 +50,19 @@ export class LintConfig {
|
|||||||
this.lineLintRules.push(maxLineLength)
|
this.lineLintRules.push(maxLineLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (json?.lineEndings) {
|
||||||
|
if (
|
||||||
|
json.lineEndings !== LineEndings.LF &&
|
||||||
|
json.lineEndings !== LineEndings.CRLF
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid value for lineEndings: can be ${LineEndings.LF} or ${LineEndings.CRLF}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.lineEndings = json.lineEndings
|
||||||
|
this.fileLintRules.push(lineEndings)
|
||||||
|
}
|
||||||
|
|
||||||
if (!isNaN(json?.indentationMultiple)) {
|
if (!isNaN(json?.indentationMultiple)) {
|
||||||
this.indentationMultiple = json.indentationMultiple as number
|
this.indentationMultiple = json.indentationMultiple as number
|
||||||
this.lineLintRules.push(indentationMultiple)
|
this.lineLintRules.push(indentationMultiple)
|
||||||
@@ -71,5 +91,9 @@ export class LintConfig {
|
|||||||
if (json?.hasMacroParentheses) {
|
if (json?.hasMacroParentheses) {
|
||||||
this.fileLintRules.push(hasMacroParentheses)
|
this.fileLintRules.push(hasMacroParentheses)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (json?.strictMacroDefinition) {
|
||||||
|
this.fileLintRules.push(strictMacroDefinition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export interface LintRule {
|
|||||||
export interface LineLintRule extends LintRule {
|
export interface LineLintRule extends LintRule {
|
||||||
type: LintRuleType.Line
|
type: LintRuleType.Line
|
||||||
test: (value: string, lineNumber: number, config?: LintConfig) => Diagnostic[]
|
test: (value: string, lineNumber: number, config?: LintConfig) => Diagnostic[]
|
||||||
|
fix?: (value: string, config?: LintConfig) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,7 +27,8 @@ export interface LineLintRule extends LintRule {
|
|||||||
*/
|
*/
|
||||||
export interface FileLintRule extends LintRule {
|
export interface FileLintRule extends LintRule {
|
||||||
type: LintRuleType.File
|
type: LintRuleType.File
|
||||||
test: (value: string) => Diagnostic[]
|
test: (value: string, config?: LintConfig) => Diagnostic[]
|
||||||
|
fix?: (value: string, config?: LintConfig) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
12
src/types/Macro.ts
Normal file
12
src/types/Macro.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
export * from './Diagnostic'
|
export * from './Diagnostic'
|
||||||
|
export * from './FormatResult'
|
||||||
export * from './LintConfig'
|
export * from './LintConfig'
|
||||||
export * from './LintRule'
|
export * from './LintRule'
|
||||||
export * from './LintRuleType'
|
export * from './LintRuleType'
|
||||||
export * from './Severity'
|
export * from './Severity'
|
||||||
|
export * from './Macro'
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import * as fileModule from '@sasjs/utils/file'
|
|||||||
import { LintConfig } from '../types/LintConfig'
|
import { LintConfig } from '../types/LintConfig'
|
||||||
import { getLintConfig } from './getLintConfig'
|
import { getLintConfig } from './getLintConfig'
|
||||||
|
|
||||||
|
const expectedFileLintRulesCount = 5
|
||||||
|
const expectedLineLintRulesCount = 5
|
||||||
|
const expectedPathLintRulesCount = 2
|
||||||
|
|
||||||
describe('getLintConfig', () => {
|
describe('getLintConfig', () => {
|
||||||
it('should get the lint config', async () => {
|
it('should get the lint config', async () => {
|
||||||
const config = await getLintConfig()
|
const config = await getLintConfig()
|
||||||
@@ -17,8 +21,8 @@ describe('getLintConfig', () => {
|
|||||||
const config = await getLintConfig()
|
const config = await getLintConfig()
|
||||||
|
|
||||||
expect(config).toBeInstanceOf(LintConfig)
|
expect(config).toBeInstanceOf(LintConfig)
|
||||||
expect(config.fileLintRules.length).toEqual(3)
|
expect(config.fileLintRules.length).toEqual(expectedFileLintRulesCount)
|
||||||
expect(config.lineLintRules.length).toEqual(5)
|
expect(config.lineLintRules.length).toEqual(expectedLineLintRulesCount)
|
||||||
expect(config.pathLintRules.length).toEqual(2)
|
expect(config.pathLintRules.length).toEqual(expectedPathLintRulesCount)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ export const DefaultLintConfiguration = {
|
|||||||
maxLineLength: 80,
|
maxLineLength: 80,
|
||||||
noTabIndentation: true,
|
noTabIndentation: true,
|
||||||
indentationMultiple: 2,
|
indentationMultiple: 2,
|
||||||
hasMacroNameInMend: false,
|
hasMacroNameInMend: true,
|
||||||
noNestedMacros: true,
|
noNestedMacros: true,
|
||||||
hasMacroParentheses: true
|
hasMacroParentheses: true,
|
||||||
|
strictMacroDefinition: true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './getLintConfig'
|
export * from './getLintConfig'
|
||||||
export * from './getProjectRoot'
|
export * from './getProjectRoot'
|
||||||
export * from './listSasFiles'
|
export * from './listSasFiles'
|
||||||
|
export * from './splitText'
|
||||||
|
|||||||
279
src/utils/parseMacros.spec.ts
Normal file
279
src/utils/parseMacros.spec.ts
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
import { LintConfig } from '../types'
|
||||||
|
import { parseMacros } from './parseMacros'
|
||||||
|
|
||||||
|
describe('parseMacros', () => {
|
||||||
|
it('should return an array with a single macro', () => {
|
||||||
|
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',
|
||||||
|
declarationLines: [' %macro test;'],
|
||||||
|
terminationLine: '%mend',
|
||||||
|
declaration: '%macro test',
|
||||||
|
termination: '%mend',
|
||||||
|
startLineNumbers: [1],
|
||||||
|
endLineNumber: 3,
|
||||||
|
parentMacro: '',
|
||||||
|
hasMacroNameInMend: 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;\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',
|
||||||
|
declarationLines: ['%macro foo;'],
|
||||||
|
terminationLine: '%mend;',
|
||||||
|
declaration: '%macro foo',
|
||||||
|
termination: '%mend',
|
||||||
|
startLineNumbers: [1],
|
||||||
|
endLineNumber: 3,
|
||||||
|
parentMacro: '',
|
||||||
|
hasMacroNameInMend: false,
|
||||||
|
mismatchedMendMacroName: ''
|
||||||
|
})
|
||||||
|
expect(macros).toContainEqual({
|
||||||
|
name: 'bar',
|
||||||
|
declarationLines: ['%macro bar();'],
|
||||||
|
terminationLine: '%mend bar;',
|
||||||
|
declaration: '%macro bar()',
|
||||||
|
termination: '%mend bar',
|
||||||
|
startLineNumbers: [4],
|
||||||
|
endLineNumber: 6,
|
||||||
|
parentMacro: '',
|
||||||
|
hasMacroNameInMend: true,
|
||||||
|
mismatchedMendMacroName: ''
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should detect nested macro definitions', () => {
|
||||||
|
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',
|
||||||
|
declarationLines: ['%macro test();'],
|
||||||
|
terminationLine: '%mend test',
|
||||||
|
declaration: '%macro test()',
|
||||||
|
termination: '%mend test',
|
||||||
|
startLineNumbers: [1],
|
||||||
|
endLineNumber: 6,
|
||||||
|
parentMacro: '',
|
||||||
|
hasMacroNameInMend: true,
|
||||||
|
mismatchedMendMacroName: ''
|
||||||
|
})
|
||||||
|
expect(macros).toContainEqual({
|
||||||
|
name: 'test2',
|
||||||
|
declarationLines: [' %macro test2;'],
|
||||||
|
terminationLine: ' %mend',
|
||||||
|
declaration: '%macro test2',
|
||||||
|
termination: '%mend',
|
||||||
|
startLineNumbers: [3],
|
||||||
|
endLineNumber: 5,
|
||||||
|
parentMacro: 'test',
|
||||||
|
hasMacroNameInMend: 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
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
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
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: ''
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
151
src/utils/parseMacros.ts
Normal file
151
src/utils/parseMacros.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { LintConfig, Macro } from '../types'
|
||||||
|
import { LineEndings } from '../types/LineEndings'
|
||||||
|
import { trimComments } from './trimComments'
|
||||||
|
|
||||||
|
export const parseMacros = (text: string, config?: LintConfig): Macro[] => {
|
||||||
|
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
|
||||||
|
const lines: string[] = text ? text.split(lineEnding) : []
|
||||||
|
const macros: Macro[] = []
|
||||||
|
|
||||||
|
let isCommentStarted = false
|
||||||
|
let macroStack: Macro[] = []
|
||||||
|
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
|
||||||
|
|
||||||
|
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 = 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,
|
||||||
|
startLineNumbers: [startLineNumber],
|
||||||
|
endLineNumber: null,
|
||||||
|
parentMacro: macroStack.length
|
||||||
|
? macroStack[macroStack.length - 1].name
|
||||||
|
: '',
|
||||||
|
hasMacroNameInMend: false,
|
||||||
|
mismatchedMendMacroName: '',
|
||||||
|
declarationLines: [line],
|
||||||
|
terminationLine: '',
|
||||||
|
declaration: trimmedStatement,
|
||||||
|
termination: ''
|
||||||
|
})
|
||||||
|
} else if (trimmedStatement.startsWith('%mend')) {
|
||||||
|
if (macroStack.length) {
|
||||||
|
const macro = macroStack.pop() as Macro
|
||||||
|
const mendMacroName =
|
||||||
|
trimmedStatement.split(' ').filter((s: string) => !!s)[1] || ''
|
||||||
|
macro.endLineNumber = lineIndex + 1
|
||||||
|
macro.hasMacroNameInMend = mendMacroName === macro.name
|
||||||
|
macro.mismatchedMendMacroName = macro.hasMacroNameInMend
|
||||||
|
? ''
|
||||||
|
: mendMacroName
|
||||||
|
macro.terminationLine = line
|
||||||
|
macro.termination = trimmedStatement
|
||||||
|
macros.push(macro)
|
||||||
|
} else {
|
||||||
|
macros.push({
|
||||||
|
name: '',
|
||||||
|
startLineNumbers: [],
|
||||||
|
endLineNumber: lineIndex + 1,
|
||||||
|
parentMacro: '',
|
||||||
|
hasMacroNameInMend: false,
|
||||||
|
mismatchedMendMacroName: '',
|
||||||
|
declarationLines: [],
|
||||||
|
terminationLine: line,
|
||||||
|
declaration: '',
|
||||||
|
termination: trimmedStatement
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
macros.push(...macroStack)
|
||||||
|
|
||||||
|
return macros
|
||||||
|
}
|
||||||
41
src/utils/splitText.spec.ts
Normal file
41
src/utils/splitText.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { LintConfig } from '../types'
|
||||||
|
import { splitText } from './splitText'
|
||||||
|
|
||||||
|
describe('splitText', () => {
|
||||||
|
const config = new LintConfig({
|
||||||
|
noTrailingSpaces: true,
|
||||||
|
noEncodedPasswords: true,
|
||||||
|
hasDoxygenHeader: true,
|
||||||
|
noSpacesInFileNames: true,
|
||||||
|
maxLineLength: 80,
|
||||||
|
lowerCaseFileNames: true,
|
||||||
|
noTabIndentation: true,
|
||||||
|
indentationMultiple: 2,
|
||||||
|
hasMacroNameInMend: true,
|
||||||
|
noNestedMacros: true,
|
||||||
|
hasMacroParentheses: true,
|
||||||
|
lineEndings: 'lf'
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an empty array when text is falsy', () => {
|
||||||
|
const lines = splitText('', config)
|
||||||
|
|
||||||
|
expect(lines.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array of lines from text', () => {
|
||||||
|
const lines = splitText(`line 1\nline 2`, config)
|
||||||
|
|
||||||
|
expect(lines.length).toEqual(2)
|
||||||
|
expect(lines[0]).toEqual('line 1')
|
||||||
|
expect(lines[1]).toEqual('line 2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should work with CRLF line endings', () => {
|
||||||
|
const lines = splitText(`line 1\r\nline 2`, config)
|
||||||
|
|
||||||
|
expect(lines.length).toEqual(2)
|
||||||
|
expect(lines[0]).toEqual('line 1')
|
||||||
|
expect(lines[1]).toEqual('line 2')
|
||||||
|
})
|
||||||
|
})
|
||||||
17
src/utils/splitText.ts
Normal file
17
src/utils/splitText.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { LintConfig } from '../types/LintConfig'
|
||||||
|
import { LineEndings } from '../types/LineEndings'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits the given content into a list of lines, regardless of CRLF or LF line endings.
|
||||||
|
* @param {string} text - the text content to be split into lines.
|
||||||
|
* @returns {string[]} an array of lines from the given text
|
||||||
|
*/
|
||||||
|
export const splitText = (text: string, config: LintConfig): string[] => {
|
||||||
|
if (!text) return []
|
||||||
|
const expectedLineEndings =
|
||||||
|
config.lineEndings === LineEndings.LF ? '\n' : '\r\n'
|
||||||
|
const incorrectLineEndings = expectedLineEndings === '\n' ? '\r\n' : '\n'
|
||||||
|
return text
|
||||||
|
.replace(new RegExp(incorrectLineEndings, 'g'), expectedLineEndings)
|
||||||
|
.split(expectedLineEndings)
|
||||||
|
}
|
||||||
@@ -7,6 +7,27 @@ describe('trimComments', () => {
|
|||||||
/* some comment */ some code;
|
/* some comment */ some code;
|
||||||
`)
|
`)
|
||||||
).toEqual({ statement: 'some code;', commentStarted: false })
|
).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', () => {
|
it('should return statment, having multi-line comment', () => {
|
||||||
|
|||||||
@@ -1,19 +1,38 @@
|
|||||||
export const trimComments = (
|
export const trimComments = (
|
||||||
statement: string,
|
statement: string,
|
||||||
commentStarted: boolean = false
|
commentStarted: boolean = false,
|
||||||
|
trimEnd: boolean = false
|
||||||
): { statement: string; commentStarted: boolean } => {
|
): { statement: string; commentStarted: boolean } => {
|
||||||
let trimmed = (statement || '').trim()
|
let trimmed = trimEnd ? (statement || '').trimEnd() : (statement || '').trim()
|
||||||
|
|
||||||
if (commentStarted || trimmed.startsWith('/*')) {
|
if (commentStarted || trimmed.startsWith('/*')) {
|
||||||
const parts = trimmed.split('*/')
|
const parts = trimmed.startsWith('/*')
|
||||||
if (parts.length > 1) {
|
? trimmed.slice(2).split('*/')
|
||||||
|
: trimmed.split('*/')
|
||||||
|
if (parts.length === 2) {
|
||||||
return {
|
return {
|
||||||
statement: (parts.pop() as string).trim(),
|
statement: (parts.pop() as string).trim(),
|
||||||
commentStarted: false
|
commentStarted: false
|
||||||
}
|
}
|
||||||
|
} else if (parts.length > 2) {
|
||||||
|
parts.shift()
|
||||||
|
return trimComments(parts.join('*/'), false)
|
||||||
} else {
|
} else {
|
||||||
return { statement: '', commentStarted: true }
|
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 }
|
return { statement: trimmed, commentStarted: false }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user