mirror of
https://github.com/sasjs/lint.git
synced 2025-12-10 17:34:36 +00:00
Compare commits
47 Commits
origin/ref
...
v1.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
c2209cbe0e | ||
|
|
fe974050f7 | ||
|
|
1402802f0a | ||
|
|
36b3a7f319 | ||
|
|
c56887d6e6 |
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
|
||||
@@ -9,6 +9,5 @@
|
||||
"indentationMultiple": 2,
|
||||
"hasMacroNameInMend": true,
|
||||
"noNestedMacros": true,
|
||||
"hasMacroParentheses": true,
|
||||
"lineEndings": "lf"
|
||||
"hasMacroParentheses": true
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
[](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.
|
||||
|
||||
93
package-lock.json
generated
93
package-lock.json
generated
@@ -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.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.5.tgz",
|
||||
"integrity": "sha512-7tP4m+silwt1NHqzNRAPjW1BswnAhopTdc2K3HEkRZjF0ZG2F/e/ypVH0xiZIMfItFtD3CX0XFbwPzp9fIEUVg==",
|
||||
"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": {
|
||||
|
||||
10
package.json
10
package.json
@@ -36,14 +36,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.5",
|
||||
"typescript": "^4.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "^2.10.1"
|
||||
"@sasjs/utils": "^2.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"noNestedMacros": true,
|
||||
"noSpacesInFileNames": true,
|
||||
"noTabIndentation": true,
|
||||
"noTrailingSpaces": true
|
||||
"noTrailingSpaces": true,
|
||||
"lineEndings": "lf"
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
@@ -29,7 +30,8 @@
|
||||
"indentationMultiple": 4,
|
||||
"hasMacroNameInMend": true,
|
||||
"noNestedMacros": true,
|
||||
"hasMacroParentheses": true
|
||||
"hasMacroParentheses": true,
|
||||
"lineEndings": "crlf"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
@@ -120,6 +122,14 @@
|
||||
"description": "Enforces no trailing spaces in lines of SAS code. Shows a warning when they are present.",
|
||||
"default": true,
|
||||
"examples": [true, false]
|
||||
},
|
||||
"lineEndings": {
|
||||
"$id": "#/properties/lineEndings",
|
||||
"type": "string",
|
||||
"title": "lineEndings",
|
||||
"description": "Enforces the configured terminating character for each line. Shows a warning when incorrect line endings are present.",
|
||||
"default": "lf",
|
||||
"examples": ["lf", "crlf"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -18,8 +18,9 @@ describe('formatText', () => {
|
||||
const expectedOutput = `/**
|
||||
@file
|
||||
@brief <Your brief here>
|
||||
<h4> SAS Macros </h4>
|
||||
**/\n%macro test
|
||||
%put 'hello';\n%mend;`
|
||||
%put 'hello';\n%mend test;`
|
||||
|
||||
const output = await formatText(text)
|
||||
|
||||
@@ -39,7 +40,7 @@ describe('formatText', () => {
|
||||
)
|
||||
const text = `%macro test\n %put 'hello';\r\n%mend; `
|
||||
|
||||
const expectedOutput = `/**\r\n @file\r\n @brief <Your brief here>\r\n**/\r\n%macro test\r\n %put 'hello';\r\n%mend;`
|
||||
const expectedOutput = `/**\r\n @file\r\n @brief <Your brief here>\r\n <h4> SAS Macros </h4>\r\n**/\r\n%macro test\r\n %put 'hello';\r\n%mend test;`
|
||||
|
||||
const output = await formatText(text)
|
||||
|
||||
|
||||
4
src/format/index.ts
Normal file
4
src/format/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './formatText'
|
||||
export * from './formatFile'
|
||||
export * from './formatFolder'
|
||||
export * from './formatProject'
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './format'
|
||||
export * from './lint'
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
|
||||
@@ -2,68 +2,87 @@ import { lintFile } from './lintFile'
|
||||
import { Severity } from '../types/Severity'
|
||||
import path from 'path'
|
||||
|
||||
const expectedDiagnostics = [
|
||||
{
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 2,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: 'File name contains spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: 'File name contains uppercase characters',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: 'File missing Doxygen header',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: 'Line contains encoded password',
|
||||
lineNumber: 5,
|
||||
startColumnNumber: 10,
|
||||
endColumnNumber: 18,
|
||||
severity: Severity.Error
|
||||
},
|
||||
{
|
||||
message: 'Line is indented with a tab',
|
||||
lineNumber: 7,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: 'Line has incorrect indentation - 3 spaces',
|
||||
lineNumber: 6,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: '%mend statement is missing macro name - mf_getuniquelibref',
|
||||
lineNumber: 17,
|
||||
startColumnNumber: 3,
|
||||
endColumnNumber: 9,
|
||||
severity: Severity.Warning
|
||||
}
|
||||
]
|
||||
|
||||
describe('lintFile', () => {
|
||||
it('should identify lint issues in a given file', async () => {
|
||||
const results = await lintFile(
|
||||
path.join(__dirname, '..', 'Example File.sas')
|
||||
)
|
||||
|
||||
expect(results.length).toEqual(8)
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 2,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'File name contains spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'File name contains uppercase characters',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'File missing Doxygen header',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line contains encoded password',
|
||||
lineNumber: 5,
|
||||
startColumnNumber: 10,
|
||||
endColumnNumber: 18,
|
||||
severity: Severity.Error
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line is indented with a tab',
|
||||
lineNumber: 7,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line has incorrect indentation - 3 spaces',
|
||||
lineNumber: 6,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results.length).toEqual(expectedDiagnostics.length)
|
||||
expect(results).toContainEqual(expectedDiagnostics[0])
|
||||
expect(results).toContainEqual(expectedDiagnostics[1])
|
||||
expect(results).toContainEqual(expectedDiagnostics[2])
|
||||
expect(results).toContainEqual(expectedDiagnostics[3])
|
||||
expect(results).toContainEqual(expectedDiagnostics[4])
|
||||
expect(results).toContainEqual(expectedDiagnostics[5])
|
||||
expect(results).toContainEqual(expectedDiagnostics[6])
|
||||
expect(results).toContainEqual(expectedDiagnostics[7])
|
||||
expect(results).toContainEqual(expectedDiagnostics[8])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,71 +1,135 @@
|
||||
import { lintFolder } from './lintFolder'
|
||||
import { Severity } from '../types/Severity'
|
||||
import path from 'path'
|
||||
import {
|
||||
createFile,
|
||||
createFolder,
|
||||
deleteFolder,
|
||||
readFile
|
||||
} from '@sasjs/utils/file'
|
||||
|
||||
const expectedFilesCount = 1
|
||||
const expectedDiagnostics = [
|
||||
{
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 2,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: 'File name contains spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: 'File name contains uppercase characters',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: 'File missing Doxygen header',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: 'Line contains encoded password',
|
||||
lineNumber: 5,
|
||||
startColumnNumber: 10,
|
||||
endColumnNumber: 18,
|
||||
severity: Severity.Error
|
||||
},
|
||||
{
|
||||
message: 'Line is indented with a tab',
|
||||
lineNumber: 7,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: 'Line has incorrect indentation - 3 spaces',
|
||||
lineNumber: 6,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: '%mend statement is missing macro name - mf_getuniquelibref',
|
||||
lineNumber: 17,
|
||||
startColumnNumber: 3,
|
||||
endColumnNumber: 9,
|
||||
severity: Severity.Warning
|
||||
}
|
||||
]
|
||||
|
||||
describe('lintFolder', () => {
|
||||
it('should identify lint issues in a given folder', async () => {
|
||||
const results = await lintFolder(path.join(__dirname, '..'))
|
||||
|
||||
expect(results.size).toEqual(1)
|
||||
const diagnostics = results.get(
|
||||
await createFolder(path.join(__dirname, 'lint-folder-test'))
|
||||
const content = await readFile(
|
||||
path.join(__dirname, '..', 'Example File.sas')
|
||||
)
|
||||
await createFile(
|
||||
path.join(__dirname, 'lint-folder-test', 'Example File.sas'),
|
||||
content
|
||||
)
|
||||
const results = await lintFolder(path.join(__dirname, 'lint-folder-test'))
|
||||
expect(results.size).toEqual(expectedFilesCount)
|
||||
const diagnostics = results.get(
|
||||
path.join(__dirname, 'lint-folder-test', 'Example File.sas')
|
||||
)!
|
||||
expect(diagnostics.length).toEqual(8)
|
||||
expect(diagnostics).toContainEqual({
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(diagnostics).toContainEqual({
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 2,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(diagnostics).toContainEqual({
|
||||
message: 'File name contains spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(diagnostics).toContainEqual({
|
||||
message: 'File name contains uppercase characters',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(diagnostics).toContainEqual({
|
||||
message: 'File missing Doxygen header',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(diagnostics).toContainEqual({
|
||||
message: 'Line contains encoded password',
|
||||
lineNumber: 5,
|
||||
startColumnNumber: 10,
|
||||
endColumnNumber: 18,
|
||||
severity: Severity.Error
|
||||
})
|
||||
expect(diagnostics).toContainEqual({
|
||||
message: 'Line is indented with a tab',
|
||||
lineNumber: 7,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(diagnostics).toContainEqual({
|
||||
message: 'Line has incorrect indentation - 3 spaces',
|
||||
lineNumber: 6,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(diagnostics.length).toEqual(expectedDiagnostics.length)
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[0])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[1])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[2])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[3])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[4])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[5])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[6])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[7])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[8])
|
||||
|
||||
await deleteFolder(path.join(__dirname, 'lint-folder-test'))
|
||||
})
|
||||
|
||||
it('should identify lint issues in subfolders of a given folder', async () => {
|
||||
await createFolder(path.join(__dirname, 'lint-folder-test'))
|
||||
await createFolder(path.join(__dirname, 'lint-folder-test', 'subfolder'))
|
||||
const content = await readFile(
|
||||
path.join(__dirname, '..', 'Example File.sas')
|
||||
)
|
||||
await createFile(
|
||||
path.join(__dirname, 'lint-folder-test', 'subfolder', 'Example File.sas'),
|
||||
content
|
||||
)
|
||||
const results = await lintFolder(path.join(__dirname, 'lint-folder-test'))
|
||||
expect(results.size).toEqual(expectedFilesCount)
|
||||
const diagnostics = results.get(
|
||||
path.join(__dirname, 'lint-folder-test', 'subfolder', 'Example File.sas')
|
||||
)!
|
||||
expect(diagnostics.length).toEqual(expectedDiagnostics.length)
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[0])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[1])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[2])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[3])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[4])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[5])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[6])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[7])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[8])
|
||||
|
||||
await deleteFolder(path.join(__dirname, 'lint-folder-test'))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,76 +2,115 @@ import { lintProject } from './lintProject'
|
||||
import { Severity } from '../types/Severity'
|
||||
import * as getProjectRootModule from '../utils/getProjectRoot'
|
||||
import path from 'path'
|
||||
import { createFolder, createFile, readFile, deleteFolder } from '@sasjs/utils'
|
||||
import { DefaultLintConfiguration } from '../utils'
|
||||
jest.mock('../utils/getProjectRoot')
|
||||
|
||||
const expectedFilesCount = 1
|
||||
const expectedDiagnostics = [
|
||||
{
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 2,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: 'File name contains spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: 'File name contains uppercase characters',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: 'File missing Doxygen header',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: 'Line contains encoded password',
|
||||
lineNumber: 5,
|
||||
startColumnNumber: 10,
|
||||
endColumnNumber: 18,
|
||||
severity: Severity.Error
|
||||
},
|
||||
{
|
||||
message: 'Line is indented with a tab',
|
||||
lineNumber: 7,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: 'Line has incorrect indentation - 3 spaces',
|
||||
lineNumber: 6,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
},
|
||||
{
|
||||
message: '%mend statement is missing macro name - mf_getuniquelibref',
|
||||
lineNumber: 17,
|
||||
startColumnNumber: 3,
|
||||
endColumnNumber: 9,
|
||||
severity: Severity.Warning
|
||||
}
|
||||
]
|
||||
|
||||
describe('lintProject', () => {
|
||||
it('should identify lint issues in a given project', async () => {
|
||||
await createFolder(path.join(__dirname, 'lint-project-test'))
|
||||
const content = await readFile(
|
||||
path.join(__dirname, '..', 'Example File.sas')
|
||||
)
|
||||
await createFile(
|
||||
path.join(__dirname, 'lint-project-test', 'Example File.sas'),
|
||||
content
|
||||
)
|
||||
await createFile(
|
||||
path.join(__dirname, 'lint-project-test', '.sasjslint'),
|
||||
JSON.stringify(DefaultLintConfiguration)
|
||||
)
|
||||
|
||||
jest
|
||||
.spyOn(getProjectRootModule, 'getProjectRoot')
|
||||
.mockImplementation(() => Promise.resolve(path.join(__dirname, '..')))
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve(path.join(__dirname, 'lint-project-test'))
|
||||
)
|
||||
const results = await lintProject()
|
||||
|
||||
expect(results.size).toEqual(1)
|
||||
expect(results.size).toEqual(expectedFilesCount)
|
||||
const diagnostics = results.get(
|
||||
path.join(__dirname, '..', 'Example File.sas')
|
||||
path.join(__dirname, 'lint-project-test', 'Example File.sas')
|
||||
)!
|
||||
expect(diagnostics.length).toEqual(8)
|
||||
expect(diagnostics).toContainEqual({
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(diagnostics).toContainEqual({
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 2,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(diagnostics).toContainEqual({
|
||||
message: 'File name contains spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(diagnostics).toContainEqual({
|
||||
message: 'File name contains uppercase characters',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(diagnostics).toContainEqual({
|
||||
message: 'File missing Doxygen header',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(diagnostics).toContainEqual({
|
||||
message: 'Line contains encoded password',
|
||||
lineNumber: 5,
|
||||
startColumnNumber: 10,
|
||||
endColumnNumber: 18,
|
||||
severity: Severity.Error
|
||||
})
|
||||
expect(diagnostics).toContainEqual({
|
||||
message: 'Line is indented with a tab',
|
||||
lineNumber: 7,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(diagnostics).toContainEqual({
|
||||
message: 'Line has incorrect indentation - 3 spaces',
|
||||
lineNumber: 6,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(diagnostics.length).toEqual(expectedDiagnostics.length)
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[0])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[1])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[2])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[3])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[4])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[5])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[6])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[7])
|
||||
expect(diagnostics).toContainEqual(expectedDiagnostics[8])
|
||||
|
||||
await deleteFolder(path.join(__dirname, 'lint-project-test'))
|
||||
})
|
||||
|
||||
it('should throw an error when a project root is not found', async () => {
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
import { lintFile, lintText } from './lint'
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* Example which tests a piece of text with all known violations.
|
||||
*/
|
||||
|
||||
const text = `/**
|
||||
@file
|
||||
@brief Returns an unused libref
|
||||
@details Use as follows:
|
||||
|
||||
libname mclib0 (work);
|
||||
libname mclib1 (work);
|
||||
libname mclib2 (work);
|
||||
|
||||
%let libref=%mf_getuniquelibref({SAS001});
|
||||
%put &=libref;
|
||||
|
||||
which returns:
|
||||
|
||||
> mclib3
|
||||
|
||||
@param prefix= first part of libref. Remember that librefs can only be 8 characters,
|
||||
so a 7 letter prefix would mean that maxtries should be 10.
|
||||
@param maxtries= the last part of the libref. Provide an integer value.
|
||||
|
||||
@version 9.2
|
||||
@author Allan Bowe
|
||||
**/
|
||||
|
||||
|
||||
%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
|
||||
%local x libref;
|
||||
%let x={SAS002};
|
||||
%do x=0 %to &maxtries;
|
||||
%if %sysfunc(libref(&prefix&x)) ne 0 %then %do;
|
||||
%let libref=&prefix&x;
|
||||
%let rc=%sysfunc(libname(&libref,%sysfunc(pathname(work))));
|
||||
%if &rc %then %put %sysfunc(sysmsg());
|
||||
&prefix&x
|
||||
%*put &sysmacroname: Libref &libref assigned as WORK and returned;
|
||||
%return;
|
||||
%end;
|
||||
%end;
|
||||
%put unable to find available libref in range &prefix.0-&maxtries;
|
||||
%mend;
|
||||
`
|
||||
|
||||
lintText(text).then((diagnostics) => {
|
||||
console.log('Text lint results:')
|
||||
console.table(diagnostics)
|
||||
})
|
||||
|
||||
lintFile(path.join(__dirname, 'Example File.sas')).then((diagnostics) => {
|
||||
console.log('File lint results:')
|
||||
console.table(diagnostics)
|
||||
})
|
||||
import { lintFile, lintText } from './lint'
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* Example which tests a piece of text with all known violations.
|
||||
*/
|
||||
|
||||
const text = `/**
|
||||
@file
|
||||
@brief Returns an unused libref
|
||||
@details Use as follows:
|
||||
|
||||
libname mclib0 (work);
|
||||
libname mclib1 (work);
|
||||
libname mclib2 (work);
|
||||
|
||||
%let libref=%mf_getuniquelibref({SAS001});
|
||||
%put &=libref;
|
||||
|
||||
which returns:
|
||||
|
||||
> mclib3
|
||||
|
||||
@param prefix= first part of libref. Remember that librefs can only be 8 characters,
|
||||
so a 7 letter prefix would mean that maxtries should be 10.
|
||||
@param maxtries= the last part of the libref. Provide an integer value.
|
||||
|
||||
@version 9.2
|
||||
@author Allan Bowe
|
||||
**/
|
||||
|
||||
|
||||
%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
|
||||
%local x libref;
|
||||
%let x={SAS002};
|
||||
%do x=0 %to &maxtries;
|
||||
%if %sysfunc(libref(&prefix&x)) ne 0 %then %do;
|
||||
%let libref=&prefix&x;
|
||||
%let rc=%sysfunc(libname(&libref,%sysfunc(pathname(work))));
|
||||
%if &rc %then %put %sysfunc(sysmsg());
|
||||
&prefix&x
|
||||
%*put &sysmacroname: Libref &libref assigned as WORK and returned;
|
||||
%return;
|
||||
%end;
|
||||
%end;
|
||||
%put unable to find available libref in range &prefix.0-&maxtries;
|
||||
%mend;
|
||||
`
|
||||
|
||||
lintText(text).then((diagnostics) => {
|
||||
console.log('Text lint results:')
|
||||
console.table(diagnostics)
|
||||
})
|
||||
|
||||
lintFile(path.join(__dirname, 'Example File.sas')).then((diagnostics) => {
|
||||
console.log('File lint results:')
|
||||
console.table(diagnostics)
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { LintConfig } from '../../types'
|
||||
import { Severity } from '../../types/Severity'
|
||||
import { hasDoxygenHeader } from './hasDoxygenHeader'
|
||||
|
||||
describe('hasDoxygenHeader', () => {
|
||||
describe('hasDoxygenHeader - test', () => {
|
||||
it('should return an empty array when the file starts with a doxygen header', () => {
|
||||
const content = `/**
|
||||
@file
|
||||
@@ -69,7 +69,9 @@ describe('hasDoxygenHeader', () => {
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasDoxygenHeader - fix', () => {
|
||||
it('should not alter the text if a doxygen header is already present', () => {
|
||||
const content = `/**
|
||||
@file
|
||||
@@ -94,6 +96,7 @@ describe('hasDoxygenHeader', () => {
|
||||
`/**
|
||||
@file
|
||||
@brief <Your brief here>
|
||||
<h4> SAS Macros </h4>
|
||||
**/` +
|
||||
'\n' +
|
||||
content
|
||||
@@ -105,7 +108,9 @@ describe('hasDoxygenHeader', () => {
|
||||
const config = new LintConfig({ lineEndings: 'crlf' })
|
||||
|
||||
expect(hasDoxygenHeader.fix!(content, config)).toEqual(
|
||||
`/**\r\n @file\r\n @brief <Your brief here>\r\n**/` + '\r\n' + content
|
||||
`/**\r\n @file\r\n @brief <Your brief here>\r\n <h4> SAS Macros </h4>\r\n**/` +
|
||||
'\r\n' +
|
||||
content
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FileLintRule } from '../../types/LintRule'
|
||||
import { LintRuleType } from '../../types/LintRuleType'
|
||||
import { Severity } from '../../types/Severity'
|
||||
|
||||
const DoxygenHeader = `/**{lineEnding} @file{lineEnding} @brief <Your brief here>{lineEnding}**/`
|
||||
const DoxygenHeader = `/**{lineEnding} @file{lineEnding} @brief <Your brief here>{lineEnding} <h4> SAS Macros </h4>{lineEnding}**/`
|
||||
|
||||
const name = 'hasDoxygenHeader'
|
||||
const description =
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { LintConfig } from '../../types'
|
||||
import { Severity } from '../../types/Severity'
|
||||
import { hasMacroNameInMend } from './hasMacroNameInMend'
|
||||
|
||||
describe('hasMacroNameInMend', () => {
|
||||
describe('hasMacroNameInMend - test', () => {
|
||||
it('should return an empty array when %mend has correct macro name', () => {
|
||||
const content = `
|
||||
%macro somemacro();
|
||||
@@ -319,4 +320,146 @@ describe('hasMacroNameInMend', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should use the configured line ending while testing content', () => {
|
||||
const content = `%macro somemacro();\r\n%put &sysmacroname;\r\n%mend;`
|
||||
|
||||
const diagnostics = hasMacroNameInMend.test(
|
||||
content,
|
||||
new LintConfig({ lineEndings: 'crlf' })
|
||||
)
|
||||
|
||||
expect(diagnostics).toEqual([
|
||||
{
|
||||
message: '%mend statement is missing macro name - somemacro',
|
||||
lineNumber: 3,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 7,
|
||||
severity: Severity.Warning
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasMacroNameInMend - fix', () => {
|
||||
it('should add macro name to the mend statement if not present', () => {
|
||||
const content = ` %macro somemacro;\n %put &sysmacroname;\n %mend;`
|
||||
const expectedContent = ` %macro somemacro;\n %put &sysmacroname;\n %mend somemacro;`
|
||||
|
||||
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
|
||||
|
||||
expect(formattedContent).toEqual(expectedContent)
|
||||
})
|
||||
|
||||
it('should add macro name to the mend statement if not present ( code in single line )', () => {
|
||||
const content = `%macro somemacro; %put &sysmacroname; %mend; some code;`
|
||||
const expectedContent = `%macro somemacro; %put &sysmacroname; %mend somemacro; some code;`
|
||||
|
||||
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
|
||||
|
||||
expect(formattedContent).toEqual(expectedContent)
|
||||
})
|
||||
|
||||
it('should add macro name to the mend statement if not present ( with multiple macros )', () => {
|
||||
const content = `
|
||||
%macro somemacro;
|
||||
%put &sysmacroname;
|
||||
%mend somemacro;
|
||||
|
||||
%macro somemacro2;
|
||||
%put &sysmacroname2;
|
||||
%mend;`
|
||||
const expectedContent = `
|
||||
%macro somemacro;
|
||||
%put &sysmacroname;
|
||||
%mend somemacro;
|
||||
|
||||
%macro somemacro2;
|
||||
%put &sysmacroname2;
|
||||
%mend somemacro2;`
|
||||
|
||||
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
|
||||
|
||||
expect(formattedContent).toEqual(expectedContent)
|
||||
})
|
||||
|
||||
it('should remove redundant %mend statement', () => {
|
||||
const content = `
|
||||
%macro somemacro;
|
||||
%put &sysmacroname;
|
||||
%mend somemacro;
|
||||
%mend something;`
|
||||
const expectedContent = `
|
||||
%macro somemacro;
|
||||
%put &sysmacroname;
|
||||
%mend somemacro;
|
||||
`
|
||||
|
||||
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
|
||||
expect(formattedContent).toEqual(expectedContent)
|
||||
})
|
||||
|
||||
it('should remove redundant %mend statement with comments', () => {
|
||||
const content = `
|
||||
%macro somemacro;
|
||||
%put &sysmacroname;
|
||||
%mend somemacro;
|
||||
/* some comment */
|
||||
/* some comment */ %mend something; some code;
|
||||
/* some comment */`
|
||||
const expectedContent = `
|
||||
%macro somemacro;
|
||||
%put &sysmacroname;
|
||||
%mend somemacro;
|
||||
/* some comment */
|
||||
/* some comment */ some code;
|
||||
/* some comment */`
|
||||
|
||||
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
|
||||
expect(formattedContent).toEqual(expectedContent)
|
||||
})
|
||||
|
||||
it('should correct mismatched macro name', () => {
|
||||
const content = `
|
||||
%macro somemacro;
|
||||
%put &sysmacroname;
|
||||
%mend someanothermacro;`
|
||||
const expectedContent = `
|
||||
%macro somemacro;
|
||||
%put &sysmacroname;
|
||||
%mend somemacro;`
|
||||
|
||||
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
|
||||
expect(formattedContent).toEqual(expectedContent)
|
||||
})
|
||||
|
||||
it('should correct mismatched macro name with comments', () => {
|
||||
const content = `
|
||||
%macro somemacro;
|
||||
/* some comments */
|
||||
%put &sysmacroname;
|
||||
/* some comments */
|
||||
%mend someanothermacro ;`
|
||||
const expectedContent = `
|
||||
%macro somemacro;
|
||||
/* some comments */
|
||||
%put &sysmacroname;
|
||||
/* some comments */
|
||||
%mend somemacro ;`
|
||||
|
||||
const formattedContent = hasMacroNameInMend.fix!(content, new LintConfig())
|
||||
expect(formattedContent).toEqual(expectedContent)
|
||||
})
|
||||
|
||||
it('should use the configured line ending while applying the fix', () => {
|
||||
const content = `%macro somemacro();\r\n%put &sysmacroname;\r\n%mend ;`
|
||||
const expectedContent = `%macro somemacro();\r\n%put &sysmacroname;\r\n%mend somemacro ;`
|
||||
|
||||
const formattedContent = hasMacroNameInMend.fix!(
|
||||
content,
|
||||
new LintConfig({ lineEndings: 'crlf' })
|
||||
)
|
||||
|
||||
expect(formattedContent).toEqual(expectedContent)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,17 +18,13 @@ const test = (value: string, config?: LintConfig) => {
|
||||
const diagnostics: Diagnostic[] = []
|
||||
macros.forEach((macro) => {
|
||||
if (macro.startLineNumber === null && macro.endLineNumber !== null) {
|
||||
const endLine = lines[macro.endLineNumber - 1]
|
||||
diagnostics.push({
|
||||
message: `%mend statement is redundant`,
|
||||
lineNumber: macro.endLineNumber,
|
||||
startColumnNumber: getColumnNumber(
|
||||
lines[macro.endLineNumber - 1],
|
||||
'%mend'
|
||||
),
|
||||
startColumnNumber: getColumnNumber(endLine, '%mend'),
|
||||
endColumnNumber:
|
||||
getColumnNumber(lines[macro.endLineNumber - 1], '%mend') +
|
||||
lines[macro.endLineNumber - 1].trim().length -
|
||||
1,
|
||||
getColumnNumber(endLine, '%mend') + macro.termination.length,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
} else if (macro.endLineNumber === null && macro.startLineNumber !== null) {
|
||||
@@ -40,35 +36,29 @@ const test = (value: string, config?: LintConfig) => {
|
||||
severity: Severity.Warning
|
||||
})
|
||||
} else if (macro.mismatchedMendMacroName) {
|
||||
const endLine = lines[(macro.endLineNumber as number) - 1]
|
||||
diagnostics.push({
|
||||
message: `%mend statement has mismatched macro name, it should be '${
|
||||
macro!.name
|
||||
}'`,
|
||||
lineNumber: macro.endLineNumber as number,
|
||||
startColumnNumber: getColumnNumber(
|
||||
lines[(macro.endLineNumber as number) - 1],
|
||||
endLine,
|
||||
macro.mismatchedMendMacroName
|
||||
),
|
||||
endColumnNumber:
|
||||
getColumnNumber(
|
||||
lines[(macro.endLineNumber as number) - 1],
|
||||
macro.mismatchedMendMacroName
|
||||
) +
|
||||
getColumnNumber(endLine, macro.mismatchedMendMacroName) +
|
||||
macro.mismatchedMendMacroName.length -
|
||||
1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
} else if (!macro.hasMacroNameInMend) {
|
||||
const endLine = lines[(macro.endLineNumber as number) - 1]
|
||||
diagnostics.push({
|
||||
message: `%mend statement is missing macro name - ${macro.name}`,
|
||||
lineNumber: macro.endLineNumber as number,
|
||||
startColumnNumber: getColumnNumber(
|
||||
lines[(macro.endLineNumber as number) - 1],
|
||||
'%mend'
|
||||
),
|
||||
endColumnNumber:
|
||||
getColumnNumber(lines[(macro.endLineNumber as number) - 1], '%mend') +
|
||||
6,
|
||||
startColumnNumber: getColumnNumber(endLine, '%mend'),
|
||||
endColumnNumber: getColumnNumber(endLine, '%mend') + 6,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
}
|
||||
@@ -79,16 +69,52 @@ const test = (value: string, config?: LintConfig) => {
|
||||
|
||||
const fix = (value: string, config?: LintConfig): string => {
|
||||
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
|
||||
let formattedText = value
|
||||
const lines: string[] = value ? value.split(lineEnding) : []
|
||||
const macros = parseMacros(value, config)
|
||||
macros
|
||||
.filter((macro) => !macro.hasMacroNameInMend)
|
||||
.forEach((macro) => {
|
||||
formattedText = formattedText.replace(
|
||||
macro.termination,
|
||||
`%mend ${macro.name};${lineEnding}`
|
||||
|
||||
macros.forEach((macro) => {
|
||||
if (macro.startLineNumber === null && macro.endLineNumber !== null) {
|
||||
// %mend statement is redundant
|
||||
const endLine = lines[macro.endLineNumber - 1]
|
||||
const startColumnNumber = getColumnNumber(endLine, '%mend')
|
||||
const endColumnNumber =
|
||||
getColumnNumber(endLine, '%mend') + macro.termination.length
|
||||
|
||||
const beforeStatement = endLine.slice(0, startColumnNumber - 1)
|
||||
const afterStatement = endLine.slice(endColumnNumber)
|
||||
lines[macro.endLineNumber - 1] = beforeStatement + afterStatement
|
||||
} else if (macro.endLineNumber === null && macro.startLineNumber !== null) {
|
||||
// missing %mend statement
|
||||
} else if (macro.mismatchedMendMacroName) {
|
||||
// mismatched macro name
|
||||
const endLine = lines[(macro.endLineNumber as number) - 1]
|
||||
const startColumnNumber = getColumnNumber(
|
||||
endLine,
|
||||
macro.mismatchedMendMacroName
|
||||
)
|
||||
})
|
||||
const endColumnNumber =
|
||||
getColumnNumber(endLine, macro.mismatchedMendMacroName) +
|
||||
macro.mismatchedMendMacroName.length -
|
||||
1
|
||||
|
||||
const beforeMacroName = endLine.slice(0, startColumnNumber - 1)
|
||||
const afterMacroName = endLine.slice(endColumnNumber)
|
||||
|
||||
lines[(macro.endLineNumber as number) - 1] =
|
||||
beforeMacroName + macro.name + afterMacroName
|
||||
} else if (!macro.hasMacroNameInMend) {
|
||||
// %mend statement is missing macro name
|
||||
const endLine = lines[(macro.endLineNumber as number) - 1]
|
||||
const startColumnNumber = getColumnNumber(endLine, '%mend')
|
||||
const endColumnNumber = getColumnNumber(endLine, '%mend') + 4
|
||||
|
||||
const beforeStatement = endLine.slice(0, startColumnNumber - 1)
|
||||
const afterStatement = endLine.slice(endColumnNumber)
|
||||
lines[(macro.endLineNumber as number) - 1] =
|
||||
beforeStatement + `%mend ${macro.name}` + afterStatement
|
||||
}
|
||||
})
|
||||
const formattedText = lines.join(lineEnding)
|
||||
|
||||
return formattedText
|
||||
}
|
||||
|
||||
@@ -44,6 +44,21 @@ describe('hasMacroParentheses', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('should return an array with a single diagnostic when macro defined without name ( single line code )', () => {
|
||||
const content = `
|
||||
%macro (); %put &sysmacroname; %mend;`
|
||||
|
||||
expect(hasMacroParentheses.test(content)).toEqual([
|
||||
{
|
||||
message: 'Macro definition missing name',
|
||||
lineNumber: 2,
|
||||
startColumnNumber: 3,
|
||||
endColumnNumber: 12,
|
||||
severity: Severity.Warning
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('should return an array with a single diagnostic when macro defined without name and parentheses', () => {
|
||||
const content = `
|
||||
%macro ;
|
||||
@@ -55,7 +70,7 @@ describe('hasMacroParentheses', () => {
|
||||
message: 'Macro definition missing name',
|
||||
lineNumber: 2,
|
||||
startColumnNumber: 3,
|
||||
endColumnNumber: 10,
|
||||
endColumnNumber: 9,
|
||||
severity: Severity.Warning
|
||||
}
|
||||
])
|
||||
|
||||
@@ -17,17 +17,19 @@ const test = (value: string, config?: LintConfig) => {
|
||||
diagnostics.push({
|
||||
message: 'Macro definition missing name',
|
||||
lineNumber: macro.startLineNumber!,
|
||||
startColumnNumber: getColumnNumber(macro.declaration, '%macro'),
|
||||
endColumnNumber: macro.declaration.length,
|
||||
startColumnNumber: getColumnNumber(macro.declarationLine, '%macro'),
|
||||
endColumnNumber:
|
||||
getColumnNumber(macro.declarationLine, '%macro') +
|
||||
macro.declaration.length,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
} else if (!macro.declaration.includes('(')) {
|
||||
} else if (!macro.declarationLine.includes('(')) {
|
||||
diagnostics.push({
|
||||
message,
|
||||
lineNumber: macro.startLineNumber!,
|
||||
startColumnNumber: getColumnNumber(macro.declaration, macro.name),
|
||||
startColumnNumber: getColumnNumber(macro.declarationLine, macro.name),
|
||||
endColumnNumber:
|
||||
getColumnNumber(macro.declaration, macro.name) +
|
||||
getColumnNumber(macro.declarationLine, macro.name) +
|
||||
macro.name.length -
|
||||
1,
|
||||
severity: Severity.Warning
|
||||
@@ -36,9 +38,9 @@ const test = (value: string, config?: LintConfig) => {
|
||||
diagnostics.push({
|
||||
message: 'Macro definition contains space(s)',
|
||||
lineNumber: macro.startLineNumber!,
|
||||
startColumnNumber: getColumnNumber(macro.declaration, macro.name),
|
||||
startColumnNumber: getColumnNumber(macro.declarationLine, macro.name),
|
||||
endColumnNumber:
|
||||
getColumnNumber(macro.declaration, macro.name) +
|
||||
getColumnNumber(macro.declarationLine, macro.name) +
|
||||
macro.name.length -
|
||||
1 +
|
||||
`()`.length,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { LintConfig, Severity } from '../../types'
|
||||
import { LineEndings } from '../../types/LineEndings'
|
||||
import { lineEndings } from './lineEndings'
|
||||
|
||||
describe('lineEndings', () => {
|
||||
describe('lineEndings - test', () => {
|
||||
it('should return an empty array when the text contains the configured line endings', () => {
|
||||
const text = "%put 'hello';\n%put 'world';\n"
|
||||
const config = new LintConfig({ lineEndings: LineEndings.LF })
|
||||
@@ -101,7 +101,9 @@ describe('lineEndings', () => {
|
||||
severity: Severity.Warning
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('lineEndings - fix', () => {
|
||||
it('should transform line endings to LF', () => {
|
||||
const text =
|
||||
"%put 'hello';\r\n%put 'test';\r\n%put 'world';\n%put 'test2';\n%put 'world2';\r\n"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { LintConfig } from '../../types'
|
||||
import { Severity } from '../../types/Severity'
|
||||
import { noNestedMacros } from './noNestedMacros'
|
||||
|
||||
@@ -73,4 +74,23 @@ describe('noNestedMacros', () => {
|
||||
|
||||
expect(noNestedMacros.test((content as unknown) as string)).toEqual([])
|
||||
})
|
||||
|
||||
it('should use the configured line ending while testing content', () => {
|
||||
const content = `%macro outer();\r\n%macro inner;\r\n%mend inner;\r\n%mend outer;`
|
||||
|
||||
const diagnostics = noNestedMacros.test(
|
||||
content,
|
||||
new LintConfig({ lineEndings: 'crlf' })
|
||||
)
|
||||
|
||||
expect(diagnostics).toEqual([
|
||||
{
|
||||
message: "Macro definition for 'inner' present in macro 'outer'",
|
||||
lineNumber: 2,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 13,
|
||||
severity: Severity.Warning
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
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[]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './Diagnostic'
|
||||
export * from './FormatResult'
|
||||
export * from './LintConfig'
|
||||
export * from './LintRule'
|
||||
export * from './LintRuleType'
|
||||
|
||||
@@ -2,6 +2,10 @@ import * as fileModule from '@sasjs/utils/file'
|
||||
import { LintConfig } from '../types/LintConfig'
|
||||
import { getLintConfig } from './getLintConfig'
|
||||
|
||||
const expectedFileLintRulesCount = 4
|
||||
const expectedLineLintRulesCount = 5
|
||||
const expectedPathLintRulesCount = 2
|
||||
|
||||
describe('getLintConfig', () => {
|
||||
it('should get the lint config', async () => {
|
||||
const config = await getLintConfig()
|
||||
@@ -17,8 +21,8 @@ describe('getLintConfig', () => {
|
||||
const config = await getLintConfig()
|
||||
|
||||
expect(config).toBeInstanceOf(LintConfig)
|
||||
expect(config.fileLintRules.length).toEqual(3)
|
||||
expect(config.lineLintRules.length).toEqual(5)
|
||||
expect(config.pathLintRules.length).toEqual(2)
|
||||
expect(config.fileLintRules.length).toEqual(expectedFileLintRulesCount)
|
||||
expect(config.lineLintRules.length).toEqual(expectedLineLintRulesCount)
|
||||
expect(config.pathLintRules.length).toEqual(expectedPathLintRulesCount)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@ export const DefaultLintConfiguration = {
|
||||
maxLineLength: 80,
|
||||
noTabIndentation: true,
|
||||
indentationMultiple: 2,
|
||||
hasMacroNameInMend: false,
|
||||
hasMacroNameInMend: true,
|
||||
noNestedMacros: true,
|
||||
hasMacroParentheses: true
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ describe('parseMacros', () => {
|
||||
expect(macros.length).toEqual(1)
|
||||
expect(macros).toContainEqual({
|
||||
name: 'test',
|
||||
declaration: '%macro test;',
|
||||
declarationLine: '%macro test;',
|
||||
terminationLine: '%mend',
|
||||
declaration: '%macro test',
|
||||
termination: '%mend',
|
||||
startLineNumber: 1,
|
||||
endLineNumber: 3,
|
||||
@@ -36,8 +38,10 @@ describe('parseMacros', () => {
|
||||
expect(macros.length).toEqual(2)
|
||||
expect(macros).toContainEqual({
|
||||
name: 'foo',
|
||||
declaration: '%macro foo;',
|
||||
termination: '%mend;',
|
||||
declarationLine: '%macro foo;',
|
||||
terminationLine: '%mend;',
|
||||
declaration: '%macro foo',
|
||||
termination: '%mend',
|
||||
startLineNumber: 1,
|
||||
endLineNumber: 3,
|
||||
parentMacro: '',
|
||||
@@ -47,8 +51,10 @@ describe('parseMacros', () => {
|
||||
})
|
||||
expect(macros).toContainEqual({
|
||||
name: 'bar',
|
||||
declaration: '%macro bar();',
|
||||
termination: '%mend bar;',
|
||||
declarationLine: '%macro bar();',
|
||||
terminationLine: '%mend bar;',
|
||||
declaration: '%macro bar()',
|
||||
termination: '%mend bar',
|
||||
startLineNumber: 4,
|
||||
endLineNumber: 6,
|
||||
parentMacro: '',
|
||||
@@ -71,6 +77,8 @@ describe('parseMacros', () => {
|
||||
expect(macros.length).toEqual(2)
|
||||
expect(macros).toContainEqual({
|
||||
name: 'test',
|
||||
declarationLine: '%macro test()',
|
||||
terminationLine: '%mend test',
|
||||
declaration: '%macro test()',
|
||||
termination: '%mend test',
|
||||
startLineNumber: 1,
|
||||
@@ -82,8 +90,10 @@ describe('parseMacros', () => {
|
||||
})
|
||||
expect(macros).toContainEqual({
|
||||
name: 'test2',
|
||||
declaration: ' %macro test2',
|
||||
termination: ' %mend',
|
||||
declarationLine: ' %macro test2',
|
||||
terminationLine: ' %mend',
|
||||
declaration: '%macro test2',
|
||||
termination: '%mend',
|
||||
startLineNumber: 3,
|
||||
endLineNumber: 5,
|
||||
parentMacro: 'test',
|
||||
|
||||
@@ -6,6 +6,8 @@ interface Macro {
|
||||
name: string
|
||||
startLineNumber: number | null
|
||||
endLineNumber: number | null
|
||||
declarationLine: string
|
||||
terminationLine: string
|
||||
declaration: string
|
||||
termination: string
|
||||
parentMacro: string
|
||||
@@ -52,7 +54,9 @@ export const parseMacros = (text: string, config?: LintConfig): Macro[] => {
|
||||
hasParentheses: trimmedStatement.endsWith('()'),
|
||||
hasMacroNameInMend: false,
|
||||
mismatchedMendMacroName: '',
|
||||
declaration: line,
|
||||
declarationLine: line,
|
||||
terminationLine: '',
|
||||
declaration: trimmedStatement,
|
||||
termination: ''
|
||||
})
|
||||
} else if (trimmedStatement.startsWith('%mend')) {
|
||||
@@ -61,11 +65,12 @@ export const parseMacros = (text: string, config?: LintConfig): Macro[] => {
|
||||
const mendMacroName =
|
||||
trimmedStatement.split(' ').filter((s: string) => !!s)[1] || ''
|
||||
macro.endLineNumber = index + 1
|
||||
macro.hasMacroNameInMend = trimmedStatement.includes(macro.name)
|
||||
macro.hasMacroNameInMend = mendMacroName === macro.name
|
||||
macro.mismatchedMendMacroName = macro.hasMacroNameInMend
|
||||
? ''
|
||||
: mendMacroName
|
||||
macro.termination = line
|
||||
macro.terminationLine = line
|
||||
macro.termination = trimmedStatement
|
||||
macros.push(macro)
|
||||
} else {
|
||||
macros.push({
|
||||
@@ -76,8 +81,10 @@ export const parseMacros = (text: string, config?: LintConfig): Macro[] => {
|
||||
hasParentheses: false,
|
||||
hasMacroNameInMend: false,
|
||||
mismatchedMendMacroName: '',
|
||||
declarationLine: '',
|
||||
terminationLine: line,
|
||||
declaration: '',
|
||||
termination: line
|
||||
termination: trimmedStatement
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user