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

Compare commits

...

51 Commits

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-20 07:00:31 +00:00
Yury Shkoda
5a358330c0 Merge pull request #50 from sasjs/git-hooks
feat(git): enabled git hook enforcing conventional commits
2021-05-20 09:01:47 +03:00
Yury Shkoda
fa9e4136bc chore(lint): fixed linting 2021-05-20 08:58:25 +03:00
Yury Shkoda
0c9b23c51b feat(git): enabled git hook enforcing conventional commits 2021-05-20 08:54:36 +03:00
Allan Bowe
9daf8f8c82 Update README.md 2021-05-06 11:02:54 +03:00
Allan Bowe
7ed846e3aa setting macronameinmend to true in readme 2021-05-06 11:02:29 +03:00
Krishna Acondy
f7f989fabd Merge pull request #44 from sasjs/dependabot/npm_and_yarn/types/node-15.0.2
chore(deps-dev): bump @types/node from 15.0.1 to 15.0.2
2021-05-06 07:26:30 +01:00
Krishna Acondy
850cf85ef1 Merge branch 'main' into dependabot/npm_and_yarn/types/node-15.0.2 2021-05-06 07:25:32 +01:00
Krishna Acondy
3dc304fffc Merge pull request #42 from sasjs/format-diagnostics
feat(format-diagnostics): add diagnostic information to format result payload
2021-05-06 07:25:10 +01:00
Krishna Acondy
e329529484 chore(*): add comment 2021-05-06 07:24:07 +01:00
Krishna Acondy
15190bfe88 Merge branch 'format-diagnostics' of https://github.com/sasjs/lint into format-diagnostics 2021-05-06 07:22:32 +01:00
Krishna Acondy
bc011c4b47 chore(*): add tests for new functionality 2021-05-06 07:22:29 +01:00
dependabot[bot]
a95c083b61 chore(deps-dev): bump @types/node from 15.0.1 to 15.0.2
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 15.0.1 to 15.0.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-05 08:27:22 +00:00
Krishna Acondy
96fb384ec9 Merge branch 'main' into format-diagnostics 2021-05-04 08:52:07 +01:00
Krishna Acondy
21fd4e8fcc Merge pull request #39 from sasjs/dependabot/add-v2-config-file
Upgrade to GitHub-native Dependabot
2021-05-04 08:51:54 +01:00
Krishna Acondy
ac595c65d0 Merge branch 'main' into dependabot/add-v2-config-file 2021-05-04 08:50:58 +01:00
Krishna Acondy
e5763ce529 Merge pull request #33 from sasjs/dependabot/npm_and_yarn/ts-jest-26.5.5
chore(deps-dev): bump ts-jest from 26.5.4 to 26.5.5
2021-05-04 08:50:29 +01:00
Krishna Acondy
4729f04589 Merge branch 'main' into dependabot/npm_and_yarn/ts-jest-26.5.5 2021-05-04 08:49:35 +01:00
dependabot-preview[bot]
596d56c906 chore(deps-dev): bump ts-jest from 26.5.4 to 26.5.5
Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 26.5.4 to 26.5.5.
- [Release notes](https://github.com/kulshekhar/ts-jest/releases)
- [Changelog](https://github.com/kulshekhar/ts-jest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kulshekhar/ts-jest/compare/v26.5.4...v26.5.5)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-05-04 07:48:11 +00:00
Krishna Acondy
32956db8b2 chore(*): Add dependabot badge 2021-05-04 08:47:30 +01:00
Krishna Acondy
7b58c455dc Merge pull request #36 from sasjs/dependabot/npm_and_yarn/types/jest-26.0.23
chore(deps-dev): bump @types/jest from 26.0.21 to 26.0.23
2021-05-04 08:46:43 +01:00
dependabot-preview[bot]
c86fd7dd1d chore(deps-dev): bump @types/jest from 26.0.21 to 26.0.23
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 26.0.21 to 26.0.23.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-05-04 07:45:39 +00:00
Krishna Acondy
34e9a7b139 Merge pull request #30 from sasjs/dependabot/npm_and_yarn/typescript-4.2.4
chore(deps-dev): bump typescript from 4.2.3 to 4.2.4
2021-05-04 08:44:11 +01:00
dependabot-preview[bot]
5de3d33c1c chore(deps-dev): bump typescript from 4.2.3 to 4.2.4
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.2.3 to 4.2.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.2.3...v4.2.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-05-04 07:41:55 +00:00
Krishna Acondy
3a6a5d30e3 Merge pull request #38 from sasjs/dependabot/npm_and_yarn/types/node-15.0.1
chore(deps-dev): bump @types/node from 14.14.35 to 15.0.1
2021-05-04 08:40:26 +01:00
Krishna Acondy
0f629c4aca chore(*): remove unnecessary ignores 2021-05-04 08:39:23 +01:00
dependabot-preview[bot]
ae4c5e8347 chore(deps-dev): bump @types/node from 14.14.35 to 15.0.1
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 14.14.35 to 15.0.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-05-04 07:38:19 +00:00
Krishna Acondy
3c700a97fc Merge branch 'main' into dependabot/add-v2-config-file 2021-05-04 08:37:03 +01:00
Krishna Acondy
d113ef4ddd Merge pull request #41 from sasjs/dependabot/npm_and_yarn/sasjs/utils-2.12.0
chore(deps): bump @sasjs/utils from 2.10.1 to 2.12.0
2021-05-04 08:36:48 +01:00
Krishna Acondy
dce9453680 feat(format-diagnostics): add diagnostic information to format result payload 2021-05-04 08:27:06 +01:00
dependabot-preview[bot]
e76abc2db2 chore(deps): bump @sasjs/utils from 2.10.1 to 2.12.0
Bumps [@sasjs/utils](https://github.com/sasjs/utils) from 2.10.1 to 2.12.0.
- [Release notes](https://github.com/sasjs/utils/releases)
- [Commits](https://github.com/sasjs/utils/compare/v2.10.1...v2.12.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-05-04 06:30:57 +00:00
dependabot-preview[bot]
1e70b9debc Upgrade to GitHub-native Dependabot 2021-04-28 22:45:36 +00:00
32 changed files with 1200 additions and 181 deletions

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

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

7
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10

View File

@@ -1,3 +1,5 @@
[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=sasjs/lint)](https://dependabot.com)
# 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.
@@ -14,7 +16,7 @@ Configuration is via a `.sasjslint` file with the following structure (these are
{
"noEncodedPasswords": true,
"hasDoxygenHeader": true,
"hasMacroNameInMend": false,
"hasMacroNameInMend": true,
"hasMacroParentheses": true,
"indentationMultiple": 2,
"lowerCaseFileNames": true,
@@ -44,7 +46,7 @@ The SASjs framework recommends the use of Doxygen headers for describing all typ
#### 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.
* Default: false (for now)
* Default: true
* Severity: WARNING
#### hasMacroParentheses

93
package-lock.json generated
View File

@@ -648,14 +648,35 @@
}
},
"@sasjs/utils": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.10.1.tgz",
"integrity": "sha512-T54jx6NEMLu2+R/ux4qcb3dDJ7nFrKkPCkmPXEfZxPQBkbq4C0kmaZv6dC63RDH68wYhoXR2S5fION5fFh91iw==",
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.12.0.tgz",
"integrity": "sha512-OnC/7R+nGI8tlSPCcI7fPyD7T97B+McnkXT0IuAYDNGbfwRPuseWq0I1h+kbAWThGT67H4hnp61N0qr8LkpHZQ==",
"requires": {
"@types/prompts": "^2.0.9",
"chalk": "^4.1.1",
"cli-table": "^0.3.6",
"consola": "^2.15.0",
"prompts": "^2.4.0",
"prompts": "^2.4.1",
"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": {
@@ -751,9 +772,9 @@
}
},
"@types/jest": {
"version": "26.0.21",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.21.tgz",
"integrity": "sha512-ab9TyM/69yg7eew9eOwKMUmvIZAKEGZYlq/dhe5/0IMUd/QLJv5ldRMdddSn+u22N13FP3s5jYyktxuBwY0kDA==",
"version": "26.0.23",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.23.tgz",
"integrity": "sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA==",
"dev": true,
"requires": {
"jest-diff": "^26.0.0",
@@ -761,9 +782,10 @@
}
},
"@types/node": {
"version": "14.14.35",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz",
"integrity": "sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag=="
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz",
"integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==",
"dev": true
},
"@types/normalize-package-data": {
"version": "2.4.0",
@@ -777,14 +799,6 @@
"integrity": "sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA==",
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
@@ -881,7 +895,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"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": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@@ -1315,7 +1336,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
@@ -1323,8 +1343,7 @@
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"colorette": {
"version": "1.2.2",
@@ -1332,6 +1351,11 @@
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
"dev": true
},
"colors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
"integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs="
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -2036,8 +2060,7 @@
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"has-value": {
"version": "1.0.0",
@@ -3587,6 +3610,7 @@
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz",
"integrity": "sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==",
"dev": true,
"requires": {
"kleur": "^3.0.3",
"sisteransi": "^1.0.5"
@@ -4395,7 +4419,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
@@ -4517,9 +4540,9 @@
}
},
"ts-jest": {
"version": "26.5.4",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.4.tgz",
"integrity": "sha512-I5Qsddo+VTm94SukBJ4cPimOoFZsYTeElR2xy6H2TOVs+NsvgYglW8KuQgKoApOKuaU/Ix/vrF9ebFZlb5D2Pg==",
"version": "26.5.6",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.6.tgz",
"integrity": "sha512-rua+rCP8DxpA8b4DQD/6X2HQS8Zy/xzViVYfEs2OQu68tkCuKLV0Md8pmX55+W24uRIyAsf/BajRfxOs+R2MKA==",
"dev": true,
"requires": {
"bs-logger": "0.x",
@@ -4535,9 +4558,9 @@
},
"dependencies": {
"semver": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
@@ -4597,9 +4620,9 @@
}
},
"typescript": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz",
"integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==",
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz",
"integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==",
"dev": true
},
"union-value": {

View File

@@ -8,7 +8,8 @@
"postpublish": "git clean -fd",
"package:lib": "npm run build && cp ./package.json build && cp README.md build && cd build && npm version \"5.0.0\" && npm pack",
"lint:fix": "npx prettier --write '{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
"lint": "npx prettier --check '{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'"
"lint": "npx prettier --check '{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
"postinstall": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true"
},
"publishConfig": {
"access": "public"
@@ -36,14 +37,14 @@
},
"homepage": "https://github.com/sasjs/lint#readme",
"devDependencies": {
"@types/jest": "^26.0.21",
"@types/node": "^14.14.35",
"@types/jest": "^26.0.23",
"@types/node": "^15.0.2",
"jest": "^26.6.3",
"rimraf": "^3.0.2",
"ts-jest": "^26.5.4",
"typescript": "^4.2.3"
"ts-jest": "^26.5.6",
"typescript": "^4.2.4"
},
"dependencies": {
"@sasjs/utils": "^2.10.1"
"@sasjs/utils": "^2.12.0"
}
}

View File

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

View File

@@ -8,22 +8,76 @@ describe('formatFile', () => {
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: []
}
await formatFile(path.join(__dirname, 'format-file-test.sas'))
const result = await readFile(path.join(__dirname, 'format-file-test.sas'))
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(expectedContent)
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 somemacro;`
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)
await formatFile(
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,
@@ -31,12 +85,13 @@ describe('formatFile', () => {
noTrailingSpaces: true
})
)
const result = await readFile(
path.join(__dirname, 'format-file-config.sas')
const formattedContent = await readFile(
path.join(__dirname, 'format-file-no-violations.sas')
)
expect(result).toEqual(expectedContent)
expect(result).toEqual(expectedResult)
expect(formattedContent).toEqual(content)
await deleteFile(path.join(__dirname, 'format-file-config.sas'))
await deleteFile(path.join(__dirname, 'format-file-no-violations.sas'))
})
})

View File

@@ -1,4 +1,6 @@
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'
@@ -7,16 +9,37 @@ import { processText } from './shared'
* Applies automatic formatting to the file at the given path.
* @param {string} filePath - the path to the file to be formatted.
* @param {LintConfig} configuration - an optional configuration. When not passed in, this is read from the .sasjslint file.
* @returns {Promise<void>} Resolves successfully when the file has been formatted.
* @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
}
}

View File

@@ -6,23 +6,39 @@ import {
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
)
await formatFolder(path.join(__dirname, 'format-folder-test'))
const result = await readFile(
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(result).toEqual(expectedContent)
expect(formattedContent).toEqual(expectedContent)
expect(result).toEqual(expectedResult)
await deleteFolder(path.join(__dirname, 'format-folder-test'))
})
@@ -30,6 +46,29 @@ describe('formatFolder', () => {
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(
@@ -42,8 +81,10 @@ describe('formatFolder', () => {
content
)
await formatFolder(path.join(__dirname, 'format-folder-test'))
const result = await readFile(
const result = await formatFolder(
path.join(__dirname, 'format-folder-test')
)
const formattedContent = await readFile(
path.join(
__dirname,
'format-folder-test',
@@ -52,7 +93,135 @@ describe('formatFolder', () => {
)
)
expect(result).toEqual(expectedContent)
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'))
})

View File

@@ -1,5 +1,7 @@
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'
@@ -19,13 +21,18 @@ const excludeFolders = [
* Automatically formats all SAS files in the folder at the given path.
* @param {string} folderPath - the path to the folder to be formatted.
* @param {LintConfig} configuration - an optional configuration. When not passed in, this is read from the .sasjslint file.
* @returns {Promise<void>} Resolves successfully when all SAS files in the given folder have been formatted.
* @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)
@@ -39,4 +46,29 @@ export const formatFolder = async (
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
}
}

View File

@@ -1,15 +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<void>} Resolves successfully when all SAS files in the current project have been formatted.
* @returns {Promise<FormatResult>} Resolves successfully when all SAS files in the current project have been formatted.
*/
export const formatProject = async () => {
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)
}

View File

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

View File

@@ -59,7 +59,7 @@ describe('hasDoxygenHeader - test', () => {
it('should return an array with a single diagnostic when the file is undefined', () => {
const content = undefined
expect(hasDoxygenHeader.test((content as unknown) as string)).toEqual([
expect(hasDoxygenHeader.test(content as unknown as string)).toEqual([
{
message: 'File missing Doxygen header',
lineNumber: 1,

View File

@@ -56,7 +56,7 @@ describe('hasMacroNameInMend - test', () => {
it('should return an array with a diagnostic for each macro missing an %mend statement', () => {
const content = `%macro somemacro;
%put &sysmacroname;
%macro othermacro`
%macro othermacro;`
expect(hasMacroNameInMend.test(content)).toEqual([
{
@@ -114,7 +114,7 @@ describe('hasMacroNameInMend - test', () => {
it('should return an empty array when the file is undefined', () => {
const content = undefined
expect(hasMacroNameInMend.test((content as unknown) as string)).toEqual([])
expect(hasMacroNameInMend.test(content as unknown as string)).toEqual([])
})
describe('nestedMacros', () => {

View File

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

View File

@@ -79,7 +79,7 @@ describe('hasMacroParentheses', () => {
it('should return an empty array when the file is undefined', () => {
const content = undefined
expect(hasMacroParentheses.test((content as unknown) as string)).toEqual([])
expect(hasMacroParentheses.test(content as unknown as string)).toEqual([])
})
describe('with extra spaces and comments', () => {
@@ -139,18 +139,4 @@ describe('hasMacroParentheses', () => {
])
})
})
it('should return an array with a single diagnostic when a macro definition contains a space', () => {
const content = `%macro test ()`
expect(hasMacroParentheses.test(content)).toEqual([
{
message: 'Macro definition contains space(s)',
lineNumber: 1,
startColumnNumber: 8,
endColumnNumber: 14,
severity: Severity.Warning
}
])
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

10
src/types/FormatResult.ts Normal file
View 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[]
}

View File

@@ -3,7 +3,8 @@ import {
hasMacroNameInMend,
noNestedMacros,
hasMacroParentheses,
lineEndings
lineEndings,
strictMacroDefinition
} from '../rules/file'
import {
indentationMultiple,
@@ -90,5 +91,9 @@ export class LintConfig {
if (json?.hasMacroParentheses) {
this.fileLintRules.push(hasMacroParentheses)
}
if (json?.strictMacroDefinition) {
this.fileLintRules.push(strictMacroDefinition)
}
}
}

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

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

View File

@@ -1,5 +1,7 @@
export * from './Diagnostic'
export * from './FormatResult'
export * from './LintConfig'
export * from './LintRule'
export * from './LintRuleType'
export * from './Severity'
export * from './Macro'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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