@sasjs/lint 4.3.2 → 4.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +22 -3
- package/.all-contributorsrc +0 -123
- package/.git-hooks/commit-msg +0 -18
- package/.git-hooks/pre-commit +0 -18
- package/.github/CONTRIBUTING.md +0 -201
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -21
- package/.github/dependabot.yml +0 -7
- package/.github/workflows/audit-watch.yml +0 -68
- package/.github/workflows/build.yml +0 -66
- package/.github/workflows/mutation-pr.yml +0 -69
- package/.github/workflows/mutation-weekly.yml +0 -92
- package/.github/workflows/publish.yml +0 -169
- package/.gitpod.yml +0 -8
- package/.pre-commit-config.yaml +0 -16
- package/.prettierrc +0 -6
- package/.sasjslint +0 -13
- package/jest.config.js +0 -62
- package/jest.stryker.config.js +0 -17
- package/mutation-baseline.json +0 -30
- package/scripts/audit-watch.mjs +0 -249
- package/scripts/generate-macro-names.mjs +0 -101
- package/scripts/mutation-gate.mjs +0 -390
- package/stryker.conf.json +0 -19
package/package.json
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"lint": "npx prettier --check \"{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
|
16
16
|
"mutation": "stryker run",
|
|
17
17
|
"mutation:changed": "node scripts/mutation-gate.mjs pr",
|
|
18
|
-
"mutation:watch": "node scripts/mutation-gate.mjs weekly"
|
|
18
|
+
"mutation:watch": "node scripts/mutation-gate.mjs weekly",
|
|
19
|
+
"check:package": "node scripts/checkPackageContents.mjs"
|
|
19
20
|
},
|
|
20
21
|
"publishConfig": {
|
|
21
22
|
"access": "public"
|
|
@@ -74,11 +75,29 @@
|
|
|
74
75
|
"ts-jest": "29.4.14",
|
|
75
76
|
"typescript": "6.0.3"
|
|
76
77
|
},
|
|
78
|
+
"files": [
|
|
79
|
+
"index.js",
|
|
80
|
+
"index.js.map",
|
|
81
|
+
"index.d.ts",
|
|
82
|
+
"checkNodeVersion.js",
|
|
83
|
+
"formatExample.js",
|
|
84
|
+
"formatExample.js.map",
|
|
85
|
+
"formatExample.d.ts",
|
|
86
|
+
"lintExample.js",
|
|
87
|
+
"lintExample.js.map",
|
|
88
|
+
"lintExample.d.ts",
|
|
89
|
+
"format",
|
|
90
|
+
"lint",
|
|
91
|
+
"rules",
|
|
92
|
+
"types",
|
|
93
|
+
"utils",
|
|
94
|
+
"sasjslint-schema.json"
|
|
95
|
+
],
|
|
77
96
|
"dependencies": {
|
|
78
|
-
"@sasjs/utils": "4.0.0",
|
|
97
|
+
"@sasjs/utils": "^4.0.0",
|
|
79
98
|
"ignore": "7.0.11"
|
|
80
99
|
},
|
|
81
|
-
"version": "4.3.
|
|
100
|
+
"version": "4.3.3",
|
|
82
101
|
"devEngines": {
|
|
83
102
|
"runtime": {
|
|
84
103
|
"name": "node",
|
package/.all-contributorsrc
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"files": [
|
|
3
|
-
"README.md"
|
|
4
|
-
],
|
|
5
|
-
"imageSize": 100,
|
|
6
|
-
"commit": false,
|
|
7
|
-
"contributors": [
|
|
8
|
-
{
|
|
9
|
-
"login": "Carus11",
|
|
10
|
-
"name": "Carus Kyle",
|
|
11
|
-
"avatar_url": "https://avatars.githubusercontent.com/u/4925828?v=4",
|
|
12
|
-
"profile": "https://github.com/Carus11",
|
|
13
|
-
"contributions": [
|
|
14
|
-
"ideas"
|
|
15
|
-
]
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
"login": "allanbowe",
|
|
19
|
-
"name": "Allan Bowe",
|
|
20
|
-
"avatar_url": "https://avatars.githubusercontent.com/u/4420615?v=4",
|
|
21
|
-
"profile": "https://github.com/allanbowe",
|
|
22
|
-
"contributions": [
|
|
23
|
-
"code",
|
|
24
|
-
"test",
|
|
25
|
-
"review",
|
|
26
|
-
"video",
|
|
27
|
-
"doc"
|
|
28
|
-
]
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"login": "YuryShkoda",
|
|
32
|
-
"name": "Yury Shkoda",
|
|
33
|
-
"avatar_url": "https://avatars.githubusercontent.com/u/25773492?v=4",
|
|
34
|
-
"profile": "https://www.erudicat.com/",
|
|
35
|
-
"contributions": [
|
|
36
|
-
"code",
|
|
37
|
-
"test",
|
|
38
|
-
"projectManagement",
|
|
39
|
-
"video",
|
|
40
|
-
"doc"
|
|
41
|
-
]
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"login": "krishna-acondy",
|
|
45
|
-
"name": "Krishna Acondy",
|
|
46
|
-
"avatar_url": "https://avatars.githubusercontent.com/u/2980428?v=4",
|
|
47
|
-
"profile": "https://krishna-acondy.io/",
|
|
48
|
-
"contributions": [
|
|
49
|
-
"code",
|
|
50
|
-
"test",
|
|
51
|
-
"review",
|
|
52
|
-
"infra",
|
|
53
|
-
"platform",
|
|
54
|
-
"maintenance",
|
|
55
|
-
"content"
|
|
56
|
-
]
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
"login": "saadjutt01",
|
|
60
|
-
"name": "Muhammad Saad ",
|
|
61
|
-
"avatar_url": "https://avatars.githubusercontent.com/u/8914650?v=4",
|
|
62
|
-
"profile": "https://github.com/saadjutt01",
|
|
63
|
-
"contributions": [
|
|
64
|
-
"code",
|
|
65
|
-
"test",
|
|
66
|
-
"review",
|
|
67
|
-
"mentoring",
|
|
68
|
-
"doc"
|
|
69
|
-
]
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
"login": "sabhas",
|
|
73
|
-
"name": "Sabir Hassan",
|
|
74
|
-
"avatar_url": "https://avatars.githubusercontent.com/u/82647447?v=4",
|
|
75
|
-
"profile": "https://github.com/sabhas",
|
|
76
|
-
"contributions": [
|
|
77
|
-
"code",
|
|
78
|
-
"test",
|
|
79
|
-
"review",
|
|
80
|
-
"ideas"
|
|
81
|
-
]
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
"login": "medjedovicm",
|
|
85
|
-
"name": "Mihajlo Medjedovic",
|
|
86
|
-
"avatar_url": "https://avatars.githubusercontent.com/u/18329105?v=4",
|
|
87
|
-
"profile": "https://github.com/medjedovicm",
|
|
88
|
-
"contributions": [
|
|
89
|
-
"code",
|
|
90
|
-
"test",
|
|
91
|
-
"review",
|
|
92
|
-
"infra"
|
|
93
|
-
]
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
"login": "VladislavParhomchik",
|
|
97
|
-
"name": "Vladislav Parhomchik",
|
|
98
|
-
"avatar_url": "https://avatars.githubusercontent.com/u/83717836?v=4",
|
|
99
|
-
"profile": "https://github.com/VladislavParhomchik",
|
|
100
|
-
"contributions": [
|
|
101
|
-
"test",
|
|
102
|
-
"review"
|
|
103
|
-
]
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
"login": "McGwire-Jones",
|
|
107
|
-
"name": "McGwire-Jones",
|
|
108
|
-
"avatar_url": "https://avatars.githubusercontent.com/u/51411005?v=4",
|
|
109
|
-
"profile": "https://github.com/McGwire-Jones",
|
|
110
|
-
"contributions": [
|
|
111
|
-
"code"
|
|
112
|
-
]
|
|
113
|
-
}
|
|
114
|
-
],
|
|
115
|
-
"contributorsPerLine": 7,
|
|
116
|
-
"projectName": "lint",
|
|
117
|
-
"projectOwner": "sasjs",
|
|
118
|
-
"repoType": "github",
|
|
119
|
-
"repoHost": "https://github.com",
|
|
120
|
-
"skipCi": true,
|
|
121
|
-
"commitConvention": "none",
|
|
122
|
-
"commitType": "docs"
|
|
123
|
-
}
|
package/.git-hooks/commit-msg
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
RED="\033[1;31m"
|
|
3
|
-
GREEN="\033[1;32m"
|
|
4
|
-
|
|
5
|
-
# Get the commit message (the parameter we're given is just the path to the
|
|
6
|
-
# temporary file which holds the message).
|
|
7
|
-
commit_message=$(cat "$1")
|
|
8
|
-
|
|
9
|
-
if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z \-]+\))?!?: .+$") then
|
|
10
|
-
echo "${GREEN} ✔ Commit message meets Conventional Commit standards"
|
|
11
|
-
exit 0
|
|
12
|
-
fi
|
|
13
|
-
|
|
14
|
-
echo "${RED}❌ Commit message does not meet the Conventional Commit standard!"
|
|
15
|
-
echo "An example of a valid message is:"
|
|
16
|
-
echo " feat(login): add the 'remember me' button"
|
|
17
|
-
echo "ℹ More details at: https://www.conventionalcommits.org/en/v1.0.0/#summary"
|
|
18
|
-
exit 1
|
package/.git-hooks/pre-commit
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
# Scan staged changes for secrets before anything else runs.
|
|
4
|
-
# The binary is pinned by the @nogoo9/gitleaks devDependency and lands in
|
|
5
|
-
# node_modules/.bin after `npm i` in the repo root. Commits are blocked until
|
|
6
|
-
# it is installed so no clone silently skips the secrets scan.
|
|
7
|
-
gitleaks_bin=node_modules/.bin/gitleaks
|
|
8
|
-
|
|
9
|
-
if [ ! -x "$gitleaks_bin" ]; then
|
|
10
|
-
echo "gitleaks not found - run 'npm i' in the repo root to install it"
|
|
11
|
-
exit 1
|
|
12
|
-
fi
|
|
13
|
-
|
|
14
|
-
if ! "$gitleaks_bin" protect --staged --redact; then
|
|
15
|
-
echo "gitleaks detected a potential secret in your staged changes"
|
|
16
|
-
echo "Remove the secret, or add a .gitleaksignore entry if it is a false positive"
|
|
17
|
-
exit 1
|
|
18
|
-
fi
|
package/.github/CONTRIBUTING.md
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
# Contributing
|
|
2
|
-
|
|
3
|
-
Contributions to `@sasjs/lint` are very welcome. This repo is a TypeScript library with no SAS dependency - it reads `.sas` files as text - so you can work on every rule, the formatter and the JSON schema with Node alone.
|
|
4
|
-
|
|
5
|
-
Please fill in the pull request template and make sure your changes are covered by tests.
|
|
6
|
-
|
|
7
|
-
## Quick start
|
|
8
|
-
|
|
9
|
-
Node 22 or later (the `engines` field), and no lifecycle scripts:
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
git clone https://github.com/sasjs/lint.git
|
|
13
|
-
cd lint
|
|
14
|
-
npm ci # .npmrc sets ignore-scripts=true, so nothing runs on install
|
|
15
|
-
git config core.hooksPath ./.git-hooks # the prepare script is one of those scripts
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Then:
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
npm test # jest WITH the coverage gate - this is the one CI runs
|
|
22
|
-
npm run lint # prettier --check over src and test
|
|
23
|
-
npx tsc --noEmit # type check
|
|
24
|
-
npm run build # emit ./build, needed by anything that requires the built library
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
`npx jest` alone does not apply the coverage threshold, so a change can look green locally and fail CI. Use `npm test`.
|
|
28
|
-
|
|
29
|
-
## Where things live
|
|
30
|
-
|
|
31
|
-
- `src/rules/file/`, `src/rules/line/`, `src/rules/path/` - one module per rule, `<name>.ts` plus `<name>.spec.ts`, each re-exported from its folder's `index.ts`.
|
|
32
|
-
- `src/lint/` - `lintText`, `lintFile`, `lintFolder`, `lintProject`, and `shared.ts` (`processText`, `processFile`, `processLine`).
|
|
33
|
-
- `src/format/` - `formatText`, `formatFile`, `formatFolder`, `formatProject`, and `shared.ts`.
|
|
34
|
-
- `src/types/` - `LintConfig.ts` (the rule registry and defaults), `LintRule.ts` (the File/Line/Path interfaces), `Diagnostic.ts`, `Severity.ts`, `LineEndings.ts`.
|
|
35
|
-
- `src/utils/` - shared helpers: `splitText`, `splitLines`, `getHeaderLinesCount`, `parseMacros`, `parseMacroCalls`, `headerMacros`, `mergeLintConfig`, `parseLintOverride`, `sasMacros`.
|
|
36
|
-
- `sasjslint-schema.json` and `README.md` - the public contract for every setting.
|
|
37
|
-
|
|
38
|
-
## Architecture
|
|
39
|
-
|
|
40
|
-
There are three types of rule:
|
|
41
|
-
|
|
42
|
-
- File rules - run once against the whole file
|
|
43
|
-
- Line rules - run once per line
|
|
44
|
-
- Path rules - run against a file path or name
|
|
45
|
-
|
|
46
|
-
A rule exports an object conforming to the `LintRule` interface: `type`, `name`, `description` and `message` at minimum. File, line and path rules also have `test`, a function returning `Diagnostic[]` for the consuming application to display. Rules that can repair what they find also have `fix`, which takes the original content - a line or the whole file - and returns it transformed. Path rules have no `fix`.
|
|
47
|
-
|
|
48
|
-
`test` reports positions as `lineNumber`, `startColumnNumber` and `endColumnNumber`. Severity comes from `config.severityLevel[name] || Severity.Warning`, so a rule must read its own name key.
|
|
49
|
-
|
|
50
|
-
## Adding a rule
|
|
51
|
-
|
|
52
|
-
Moving the check into an existing rule versus adding a new one is a design call. A rule that must be separately switchable cannot be folded into another, because `severityLevel` is per rule NAME - one name means one opt-out. Check what the neighbouring rule's registration condition is first: a rule that only registers when a key is an explicit non-`off` value drags its whole remit along with any check folded in.
|
|
53
|
-
|
|
54
|
-
When a new rule is the right answer:
|
|
55
|
-
|
|
56
|
-
1. Add `src/rules/<type>/X.ts` and `X.spec.ts`.
|
|
57
|
-
2. Export it from `src/rules/<type>/index.ts`.
|
|
58
|
-
3. Register it in `src/types/LintConfig.ts`, choosing the default posture:
|
|
59
|
-
- `if (json?.X !== false)` pushes a rule that is ON by default;
|
|
60
|
-
- `if (json?.X)` pushes one that is OFF by default.
|
|
61
|
-
A rule whose `fix` must run during autoformat has to be ON, because the fix only runs for rules present in the config.
|
|
62
|
-
4. Add the key to the README config block and a `### X` section in alphabetical position, with `Default:` and `Severity:`.
|
|
63
|
-
5. Add the schema entries in `sasjslint-schema.json`: the `default` object, the `examples` object, a `properties.X` block, and a `severityLevel.properties.X` entry.
|
|
64
|
-
6. Add specs for both the default-on and the default-off path.
|
|
65
|
-
|
|
66
|
-
There is no "warn but do not fix" state. A rule with a `fix` either has both warning and repairing active, or is absent and does neither. If your `fix` rewrites files, defaulting it ON changes the output of `sasjs lint fix` and format-on-save for every existing user, so say so in the PR and mark the commit accordingly.
|
|
67
|
-
|
|
68
|
-
## The three documents that must agree
|
|
69
|
-
|
|
70
|
-
`DefaultLintConfiguration` (`src/utils/getLintConfig.ts`), the fenced config block in `README.md`, and the `default` object in `sasjslint-schema.json` state the same thing, and they drift. Treat them as one artifact: complete the default object first, then mirror it into both documents. `src/utils/getLintConfig.spec.ts` fails if any of the three disagree, and also enumerates the settings the constructor reads, so a setting added to `LintConfig` but not to the defaults is caught. An absent key must mean the documented default - a project with no `.sasjslint` and a project whose file omits the key must get the same rule set.
|
|
71
|
-
|
|
72
|
-
## Reading a file: one split for line endings
|
|
73
|
-
|
|
74
|
-
Readers must not split content on the ending CONFIGURED in `lineEndings`. That value says what the project wants, not what the file contains, and the lint path does not normalise - so a `crlf` split over an LF file returns one string holding the whole file, and column lookups then throw. Use `splitLines` (`src/utils/splitLines.ts`), which splits on `\r\n` or `\n` and drops the carriage returns. Writers still join with the configured ending.
|
|
75
|
-
|
|
76
|
-
The formatter currently survives the mismatch because `lineEndings.fix` runs before the later rules, but that is not a licence for a reader to assume it: `formatFile` lints before it formats.
|
|
77
|
-
|
|
78
|
-
## Formatter rules
|
|
79
|
-
|
|
80
|
-
Only file and line rules that carry a `fix` run during formatting, in the order they were pushed onto the config. `hasDoxygenHeader` is pushed first, so with the default config a `fix` may assume a Doxygen header exists - but it must still return the text unchanged when there is none.
|
|
81
|
-
|
|
82
|
-
A `fix` must be idempotent: format the same file twice and the second pass must change nothing. Formatting a corpus twice and comparing is the quickest way to prove it. A non-idempotent fix shows up as churn and an inflated `fixedDiagnosticsCount`.
|
|
83
|
-
|
|
84
|
-
Two rules can reconcile the same header from opposite sides - `noUndeclaredMacros` adds used-but-undeclared names and `noUnusedMacros` removes declared-but-unused ones - so a removal helper must keep the `<h4> SAS Macros </h4>` heading when the list empties, or the add side loses its place to write.
|
|
85
|
-
|
|
86
|
-
## Testing
|
|
87
|
-
|
|
88
|
-
Most of the code is pure functions, so there is little to mock and no excuse for not testing a rule. When adding a rule, test the positive and negative scenarios in separate cases; when changing one, run the suite to prove nothing else moved.
|
|
89
|
-
|
|
90
|
-
Two habits catch the failures that plain coverage does not:
|
|
91
|
-
|
|
92
|
-
- **Mutation-check your assertions.** Revert the behaviour one piece at a time and confirm a test fails. An assertion that passes on the broken code is documenting nothing. Watch for vacuous cases: for example, a "does not throw" test over a mis-split file passes while the mis-split still contains the string being searched for, so the fixture has to be shaped to fail.
|
|
93
|
-
- **A negative test must fail for the reason it names.** Assert on the specific message or error, never a bare `toThrow()`.
|
|
94
|
-
|
|
95
|
-
### Mutation testing
|
|
96
|
-
|
|
97
|
-
The habit above is automated, because the mutant you think to try is rarely the
|
|
98
|
-
one that matters. Stryker rewrites the source in small ways (an operator, a
|
|
99
|
-
string, a branch) and runs the tests; a mutant that survives is behaviour no
|
|
100
|
-
test asserts.
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
npm run mutation:changed # only the files this branch changed, vs the baseline
|
|
104
|
-
MUTATION_FILES=src/utils/parseMacroCalls.ts npm run mutation:changed # one file
|
|
105
|
-
npm run mutation # whatever stryker.conf.json selects
|
|
106
|
-
npm run mutation:watch # the whole tree; prints a refreshed baseline
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
The gate compares each file with `mutation-baseline.json` and fails when a file
|
|
110
|
-
carries MORE surviving mutants than the baseline records, so it asks "did this
|
|
111
|
-
branch add an untested behaviour?", not "is the repo perfect yet". A file with no
|
|
112
|
-
baseline must reach 80%. `mutation-pr.yml` runs the changed-file gate on every
|
|
113
|
-
pull request; `mutation-weekly.yml` runs the whole tree and fails if anything
|
|
114
|
-
regressed, which is how the baseline is refreshed (the run prints the new JSON to
|
|
115
|
-
paste in).
|
|
116
|
-
|
|
117
|
-
Two things to know before running it:
|
|
118
|
-
|
|
119
|
-
- the dev tree needs Node 22.18 or newer: `@stryker-mutator/core` pulls `@babel/*` 8,
|
|
120
|
-
whose `engines` start at 22.18. `package.json` records that in `devEngines` (dev installs
|
|
121
|
-
warn, published installs are unaffected - the tarball carries no devDependencies).
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
- `jest.stryker.config.js` differs from `jest.config.js` in switching coverage
|
|
125
|
-
off, and in nothing else: a skipped spec records the mutants it covers as
|
|
126
|
-
legitimate survivors, and the weekly refresh writes them into the baseline.
|
|
127
|
-
`isIgnored` therefore derives its path with `path.relative(root, file)` and
|
|
128
|
-
returns false for a path outside the root, and `isIgnored.spec.ts` pins
|
|
129
|
-
`getProjectRoot()` with a jest mock - the `ignore` package rejects an absolute
|
|
130
|
-
or `..`-prefixed path, which is what a sandbox run produced before.
|
|
131
|
-
- a run mutates the file(s) in a sandbox under `.stryker-tmp`, and `jest.config.js`
|
|
132
|
-
ignores that directory by a `<rootDir>`-anchored pattern - an unanchored one
|
|
133
|
-
makes Stryker's own dry run collect nothing.
|
|
134
|
-
|
|
135
|
-
A surviving mutant is either a missing test or an EQUIVALENT mutant (a change
|
|
136
|
-
that cannot alter behaviour, such as dropping a check the following regular
|
|
137
|
-
expression repeats). Add the spec that fails on it, or say in the PR body why it
|
|
138
|
-
is equivalent - do not lower the baseline.
|
|
139
|
-
|
|
140
|
-
## Regenerating the shipped macro list
|
|
141
|
-
|
|
142
|
-
The macros SAS ships are language data, owned by `@sasjs/sas-language`. Do not hand-write them here. `npm run generate:macros` reads its `macroStatements` and `macroFunctions` groups and writes `src/utils/sasMacros.generated.ts`, which is committed so the published package carries no runtime dependency. `sasMacros.spec.ts` reads the same data independently and fails when the committed list differs, which is the drift guard. Extending that vocabulary is a PR to `@sasjs/sas-language`.
|
|
143
|
-
|
|
144
|
-
## Code style
|
|
145
|
-
|
|
146
|
-
The repo uses `Prettier`. In VS Code, install the Prettier extension and set `editor.formatOnSave` (and `editor.formatOnPaste`) to `true`. Otherwise run `npm run lint:fix` before committing, and `npm run lint` to check.
|
|
147
|
-
|
|
148
|
-
## Git hooks
|
|
149
|
-
|
|
150
|
-
The repo ships its own hooks in [`.git-hooks/`](../.git-hooks):
|
|
151
|
-
|
|
152
|
-
- `pre-commit` - scans staged changes for secrets with gitleaks, and requires the `@nogoo9/gitleaks` devDependency to be installed
|
|
153
|
-
- `commit-msg` - verifies the commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) standard
|
|
154
|
-
|
|
155
|
-
The repo `.npmrc` sets `ignore-scripts=true`, so the `prepare` script that would normally set `core.hooksPath` during `npm i` never runs. After cloning (or if your commits are not being checked), activate the hooks with a one-time command, run from the repo root:
|
|
156
|
-
|
|
157
|
-
```bash
|
|
158
|
-
git config core.hooksPath ./.git-hooks
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
The pre-commit hook runs the gitleaks binary provided by the `@nogoo9/gitleaks` devDependency, so make sure `npm i` has run in the repo root before your first commit.
|
|
162
|
-
|
|
163
|
-
If you use the [pre-commit](https://pre-commit.com) framework, the repo also ships a [`.pre-commit-config.yaml`](../.pre-commit-config.yaml) that pins the upstream gitleaks hook to an exact release tag. Activate it with `pre-commit install`.
|
|
164
|
-
|
|
165
|
-
## Commit messages and releases
|
|
166
|
-
|
|
167
|
-
Merging to `main` runs semantic-release: the commit type decides the version, and a release publishes to npm. Only `feat:`, `fix:` and `perf:` cut a release - a `chore:` or `docs:` PR publishes nothing, which is correct rather than a failure.
|
|
168
|
-
|
|
169
|
-
For a squash merge the PR TITLE drives the version, so keep it conventional. Judge the type by what users see: a new diagnostic message, or a formatter that now rewrites files it used to leave alone, is new behaviour and belongs in `feat:` even when it also fixes a defect. Warnings do not move the exit code of `sasjs lint` - only `noEncodedPasswords` ships as an error - so a new warning does not break CI for consumers; a rewritten file might surprise them, and the release notes are where to say so.
|
|
170
|
-
|
|
171
|
-
Mark a genuinely breaking change with `BREAKING CHANGE:` in the commit body, and name the config key that restores the old behaviour.
|
|
172
|
-
|
|
173
|
-
## The audit gate
|
|
174
|
-
|
|
175
|
-
`build.yml` runs `npm audit --omit=dev --audit-level=low`, scoped to production dependencies because the dev tree carries one advisory nothing here can fix (`sprintf-js` through the jest transform chain, with no patched release). A weekly `audit-watch.yml` runs the FULL audit and fails when that exemption stops matching reality - a new dev-tree advisory, the exempted one disappearing, or a watched package publishing a newer version.
|
|
176
|
-
|
|
177
|
-
Do not add a dev-tree advisory to the exemption to get a build green. Prefer removing the dependency; if an exemption is genuinely unavoidable, it needs the reachability argument written down and a guard that closes it, not an allowlist.
|
|
178
|
-
|
|
179
|
-
## CI is the enforcement point
|
|
180
|
-
|
|
181
|
-
The [build workflow](./workflows/build.yml) scans the full history for secrets with gitleaks, runs `npm audit`, checks code style and runs the tests. The local hooks are convenience - CI is what a red finding must fail.
|
|
182
|
-
|
|
183
|
-
## The release toolchain lives in the workflow
|
|
184
|
-
|
|
185
|
-
semantic-release is not a project dependency. Every `@semantic-release` plugin declares a `semantic-release` peer, so adding one to `devDependencies` makes npm auto-install semantic-release along with the `npm` package it bundles - and that bundled npm carries advisories this repo cannot fix.
|
|
186
|
-
|
|
187
|
-
Instead, [publish.yml](./workflows/publish.yml) installs semantic-release, `@semantic-release/exec` and `@semantic-release/git` on the fly, pinned to exact versions. When you add or remove an entry in `release.plugins` (package.json), update that command to match.
|
|
188
|
-
|
|
189
|
-
## What to work on
|
|
190
|
-
|
|
191
|
-
The formatter applies these fixes today: `%mend` macro names, a Doxygen header template, adding undeclared macros to the `<h4> SAS Macros </h4>` section, removing unused macros from it, trailing spaces, and line endings (including a missing final newline) when [`lineEndings`](../README.md#lineendings) is configured.
|
|
192
|
-
|
|
193
|
-
Fixes not yet implemented, roughly in order of usefulness:
|
|
194
|
-
|
|
195
|
-
- Change tabs to spaces
|
|
196
|
-
- Remove gremlin characters
|
|
197
|
-
- Convert single asterisk comments to block comments
|
|
198
|
-
|
|
199
|
-
Harder, and still under investigation: automatic indentation and code layout.
|
|
200
|
-
|
|
201
|
-
Rules without a fix are equally welcome - a rule that reports something real is worth more than one that silently repairs it wrong. Open an issue describing the SAS construct and what a false positive would look like before you build it, so the heuristic can be argued about cheaply.
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
## Issue
|
|
2
|
-
|
|
3
|
-
Link any related issue(s) in this section.
|
|
4
|
-
|
|
5
|
-
## Intent
|
|
6
|
-
|
|
7
|
-
What this PR intends to achieve.
|
|
8
|
-
|
|
9
|
-
## Implementation
|
|
10
|
-
|
|
11
|
-
What code changes have been made to achieve the intent.
|
|
12
|
-
|
|
13
|
-
## Checks
|
|
14
|
-
|
|
15
|
-
- [ ] Code is formatted correctly (`npm run lint:fix`).
|
|
16
|
-
- [ ] Any new functionality has been unit tested.
|
|
17
|
-
- [ ] All unit tests are passing (`npm test`).
|
|
18
|
-
- [ ] All CI checks are green.
|
|
19
|
-
- [ ] sasjslint-schema.json is updated with any new / changed functionality
|
|
20
|
-
- [ ] JSDoc comments have been added or updated.
|
|
21
|
-
- [ ] Reviewer is assigned.
|
package/.github/dependabot.yml
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
name: Audit Watch
|
|
2
|
-
|
|
3
|
-
# The build gate runs `npm audit --omit=dev`, because the dev tree carries one
|
|
4
|
-
# advisory that cannot be fixed from here (GHSA-hp3w-g68c-fv3c, sprintf-js - see
|
|
5
|
-
# the Security Audit step in build.yml). Nothing in build.yml re-checks the dev
|
|
6
|
-
# tree, so that exemption would otherwise be open-ended. This runs the full audit
|
|
7
|
-
# weekly and fails when the exemption stops matching reality: a new dev-tree
|
|
8
|
-
# advisory appears, an exempted one disappears, or a package named in the
|
|
9
|
-
# exemption publishes a newer release. Any of those files an issue - and only
|
|
10
|
-
# those, because a scheduled job that is red every week trains people to ignore
|
|
11
|
-
# red.
|
|
12
|
-
|
|
13
|
-
on:
|
|
14
|
-
schedule:
|
|
15
|
-
- cron: '0 8 * * 1' # Mondays, 08:00 UTC
|
|
16
|
-
workflow_dispatch:
|
|
17
|
-
|
|
18
|
-
jobs:
|
|
19
|
-
watch:
|
|
20
|
-
runs-on: ubuntu-latest
|
|
21
|
-
permissions:
|
|
22
|
-
contents: read
|
|
23
|
-
issues: write
|
|
24
|
-
|
|
25
|
-
steps:
|
|
26
|
-
- uses: actions/checkout@v4
|
|
27
|
-
|
|
28
|
-
- name: Use Node.js
|
|
29
|
-
uses: actions/setup-node@v4
|
|
30
|
-
with:
|
|
31
|
-
node-version: 22
|
|
32
|
-
|
|
33
|
-
- name: Install Dependencies
|
|
34
|
-
run: npm ci
|
|
35
|
-
|
|
36
|
-
- name: Re-check the dev-tree audit exemption
|
|
37
|
-
id: watch
|
|
38
|
-
env:
|
|
39
|
-
AUDIT_WATCH_REPORT: ${{ runner.temp }}/audit-watch-report.md
|
|
40
|
-
run: node scripts/audit-watch.mjs
|
|
41
|
-
|
|
42
|
-
- name: File an issue
|
|
43
|
-
# `always()` because the watched step has already failed, which would
|
|
44
|
-
# otherwise skip this one.
|
|
45
|
-
if: always() && steps.watch.outcome == 'failure'
|
|
46
|
-
env:
|
|
47
|
-
GH_TOKEN: ${{ github.token }}
|
|
48
|
-
REPORT: ${{ runner.temp }}/audit-watch-report.md
|
|
49
|
-
run: |
|
|
50
|
-
title='Audit watch: the dev-tree exemption needs re-evaluating'
|
|
51
|
-
existing=$(gh issue list --state open --search "\"$title\" in:title" \
|
|
52
|
-
--json number --jq '.[0].number // empty')
|
|
53
|
-
|
|
54
|
-
if [ -n "$existing" ]; then
|
|
55
|
-
echo "already tracked in #$existing"
|
|
56
|
-
exit 0
|
|
57
|
-
fi
|
|
58
|
-
|
|
59
|
-
if [ -f "$REPORT" ]; then
|
|
60
|
-
gh issue create --title "$title" --body-file "$REPORT"
|
|
61
|
-
else
|
|
62
|
-
# The script writes a report even when the watch itself breaks, so
|
|
63
|
-
# this means it never started. File the issue anyway rather than
|
|
64
|
-
# failing on a missing path and filing nothing.
|
|
65
|
-
gh issue create --title "$title" --body \
|
|
66
|
-
"The audit watch step failed before producing a report. Check the run:
|
|
67
|
-
$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
|
|
68
|
-
fi
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
|
2
|
-
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
|
3
|
-
|
|
4
|
-
name: SASjs Lint Build
|
|
5
|
-
|
|
6
|
-
on:
|
|
7
|
-
push:
|
|
8
|
-
pull_request:
|
|
9
|
-
|
|
10
|
-
jobs:
|
|
11
|
-
build:
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
|
|
14
|
-
strategy:
|
|
15
|
-
matrix:
|
|
16
|
-
# The floor is what the manifest declares; lts/* tracks the newest LTS.
|
|
17
|
-
# Testing only lts/* would let a change that breaks node 22 ship, since
|
|
18
|
-
# lts/* already resolves to 24.
|
|
19
|
-
node-version: [22, lts/*]
|
|
20
|
-
|
|
21
|
-
steps:
|
|
22
|
-
- uses: actions/checkout@v4
|
|
23
|
-
- name: Use Node.js ${{ matrix.node-version }}
|
|
24
|
-
uses: actions/setup-node@v4
|
|
25
|
-
with:
|
|
26
|
-
node-version: ${{ matrix.node-version }}
|
|
27
|
-
cache: npm
|
|
28
|
-
- name: Install Dependencies
|
|
29
|
-
run: npm ci
|
|
30
|
-
- name: Scan for secrets (gitleaks)
|
|
31
|
-
# The local pre-commit hook is suppressed entirely when .npmrc sets
|
|
32
|
-
# ignore-scripts=true (the prepare script that would activate the
|
|
33
|
-
# hooks never runs), so CI is the enforcement point: a leaked
|
|
34
|
-
# credential must fail the build, not just a commit.
|
|
35
|
-
# Uses the same @nogoo9/gitleaks binary the pre-commit hook pins - the
|
|
36
|
-
# gitleaks-action requires a license key for organization repos.
|
|
37
|
-
run: node_modules/.bin/gitleaks detect --source . --redact --no-banner -v
|
|
38
|
-
- name: Security Audit
|
|
39
|
-
# Prod-scoped on purpose: this gate covers what actually ships. The
|
|
40
|
-
# release toolchain is not a project dependency (see publish.yml), so
|
|
41
|
-
# the `npm` package bundled by @semantic-release/npm never reaches this
|
|
42
|
-
# tree - which is what made the old exemption-based audit script
|
|
43
|
-
# necessary. The dev tree carries one advisory that cannot be fixed
|
|
44
|
-
# from here: GHSA-hp3w-g68c-fv3c, sprintf-js reached through
|
|
45
|
-
# @jest/transform -> babel-plugin-istanbul ->
|
|
46
|
-
# @istanbuljs/load-nyc-config -> js-yaml 3 -> argparse. Every package in
|
|
47
|
-
# that chain is at its latest published version, and the advisory lists
|
|
48
|
-
# no patched release at all (sprintf-js 1.1.3 is the newest and is
|
|
49
|
-
# still in range), so gating on it would mean a permanently red build.
|
|
50
|
-
# `npm audit fix --force` does not fix it either - it downgrades jest
|
|
51
|
-
# 30 -> 25 and takes the tree from 21 advisories to 45.
|
|
52
|
-
#
|
|
53
|
-
# The exemption is not open-ended: audit-watch.yml runs the full audit
|
|
54
|
-
# weekly and fails when this comment stops matching the tree - a new
|
|
55
|
-
# dev-tree advisory, this one disappearing, or sprintf-js publishing a
|
|
56
|
-
# newer release. Keep the GHSA id named here and in the EXEMPTIONS list
|
|
57
|
-
# of scripts/audit-watch.mjs.
|
|
58
|
-
run: npm audit --omit=dev --audit-level=low
|
|
59
|
-
- name: Check Code Style
|
|
60
|
-
run: npm run lint
|
|
61
|
-
- name: Run Unit Tests
|
|
62
|
-
run: npm test
|
|
63
|
-
- name: Build Package
|
|
64
|
-
run: npm run package:lib
|
|
65
|
-
env:
|
|
66
|
-
CI: true
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
name: Mutation (changed files)
|
|
2
|
-
|
|
3
|
-
# Mutation testing answers a question coverage cannot: does any test FAIL when
|
|
4
|
-
# the code is wrong? Stryker flips an operator or a string in the source and runs
|
|
5
|
-
# the tests; a mutant that survives is behaviour nothing asserts.
|
|
6
|
-
#
|
|
7
|
-
# This job mutates only the source files the PR changed, and compares them with
|
|
8
|
-
# mutation-baseline.json - so it asks "did this branch add an untested
|
|
9
|
-
# behaviour?", not "is the repo perfect yet". The baseline is refreshed by
|
|
10
|
-
# mutation-weekly.yml.
|
|
11
|
-
#
|
|
12
|
-
# Cost control: Stryker runs the tests that cover the mutated file
|
|
13
|
-
# (`enableFindRelatedTests`), and only the mutated file is instrumented. A
|
|
14
|
-
# two-file run takes minutes, not the hours a whole-suite run per mutant would.
|
|
15
|
-
|
|
16
|
-
on:
|
|
17
|
-
pull_request:
|
|
18
|
-
paths:
|
|
19
|
-
- 'src/**'
|
|
20
|
-
- 'jest.config.js'
|
|
21
|
-
- 'jest.stryker.config.js'
|
|
22
|
-
- 'stryker.conf.json'
|
|
23
|
-
- 'mutation-baseline.json'
|
|
24
|
-
- 'scripts/mutation-gate.mjs'
|
|
25
|
-
- 'package.json'
|
|
26
|
-
- 'package-lock.json'
|
|
27
|
-
|
|
28
|
-
# a second push supersedes the first run rather than queueing behind it
|
|
29
|
-
concurrency:
|
|
30
|
-
group: mutation-${{ github.event.pull_request.number }}
|
|
31
|
-
cancel-in-progress: true
|
|
32
|
-
|
|
33
|
-
jobs:
|
|
34
|
-
mutation:
|
|
35
|
-
runs-on: ubuntu-latest
|
|
36
|
-
# checkout needs read, and the job writes nothing else
|
|
37
|
-
permissions:
|
|
38
|
-
contents: read
|
|
39
|
-
# a changed-file run takes minutes; the default cap is six hours, which would
|
|
40
|
-
# hold a runner on a stuck mutation run
|
|
41
|
-
timeout-minutes: 20
|
|
42
|
-
|
|
43
|
-
steps:
|
|
44
|
-
- uses: actions/checkout@v4
|
|
45
|
-
with:
|
|
46
|
-
# the diff against the base commit needs the base commit
|
|
47
|
-
fetch-depth: 0
|
|
48
|
-
|
|
49
|
-
- name: Use Node.js
|
|
50
|
-
uses: actions/setup-node@v4
|
|
51
|
-
with:
|
|
52
|
-
node-version: 22
|
|
53
|
-
cache: npm
|
|
54
|
-
|
|
55
|
-
- name: Install Dependencies
|
|
56
|
-
run: npm ci
|
|
57
|
-
|
|
58
|
-
- name: Mutate the changed files
|
|
59
|
-
env:
|
|
60
|
-
MUTATION_BASE: ${{ github.event.pull_request.base.sha }}
|
|
61
|
-
run: node scripts/mutation-gate.mjs pr
|
|
62
|
-
|
|
63
|
-
- name: Keep the report
|
|
64
|
-
if: always()
|
|
65
|
-
uses: actions/upload-artifact@v4
|
|
66
|
-
with:
|
|
67
|
-
name: mutation-report
|
|
68
|
-
path: reports/mutation/mutation.json
|
|
69
|
-
if-no-files-found: ignore
|