@sasjs/lint 4.3.0 → 4.3.2
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/.github/CONTRIBUTING.md +45 -0
- package/.github/workflows/mutation-pr.yml +69 -0
- package/.github/workflows/mutation-weekly.yml +92 -0
- package/README.md +15 -3
- package/jest.config.js +49 -0
- package/jest.stryker.config.js +17 -0
- package/mutation-baseline.json +30 -0
- package/package.json +14 -2
- package/rules/file/noUndeclaredMacros.js +9 -2
- package/rules/file/noUndeclaredMacros.js.map +1 -1
- package/rules/file/noUnusedMacros.js +15 -7
- package/rules/file/noUnusedMacros.js.map +1 -1
- package/sasjslint-schema.json +1 -1
- package/scripts/mutation-gate.mjs +390 -0
- package/stryker.conf.json +19 -0
- package/utils/headerMacros.d.ts +22 -6
- package/utils/headerMacros.js +135 -43
- package/utils/headerMacros.js.map +1 -1
- package/utils/isIgnored.js +18 -3
- package/utils/isIgnored.js.map +1 -1
- package/utils/parseMacroCalls.d.ts +24 -3
- package/utils/parseMacroCalls.js +64 -4
- package/utils/parseMacroCalls.js.map +1 -1
package/.github/CONTRIBUTING.md
CHANGED
|
@@ -92,6 +92,51 @@ Two habits catch the failures that plain coverage does not:
|
|
|
92
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
93
|
- **A negative test must fail for the reason it names.** Assert on the specific message or error, never a bare `toThrow()`.
|
|
94
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
|
+
|
|
95
140
|
## Regenerating the shipped macro list
|
|
96
141
|
|
|
97
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`.
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
name: Mutation Watch
|
|
2
|
+
|
|
3
|
+
# The per-PR mutation gate compares a branch's changed files with a baseline, so
|
|
4
|
+
# the baseline is the thing that has to stay honest: if it is never refreshed,
|
|
5
|
+
# files that improve keep their old count and the gate stops asking for more.
|
|
6
|
+
#
|
|
7
|
+
# This runs the whole source tree weekly, prints the new baseline, and fails when
|
|
8
|
+
# any file carries MORE surviving mutants than the recorded one - which means a
|
|
9
|
+
# PR was merged without the gate seeing it (a push to main, a config change, a
|
|
10
|
+
# file the gate skipped). Failure files an issue, as audit-watch.yml does, and
|
|
11
|
+
# only a genuine regression does so.
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
schedule:
|
|
15
|
+
- cron: '0 7 * * 1' # Mondays, 07:00 UTC
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
watch:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
# the whole tree takes ~20-30 minutes; bounded so a hang cannot hold a runner
|
|
22
|
+
# for the six-hour default
|
|
23
|
+
timeout-minutes: 90
|
|
24
|
+
permissions:
|
|
25
|
+
contents: read
|
|
26
|
+
issues: write
|
|
27
|
+
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
|
|
31
|
+
- name: Use Node.js
|
|
32
|
+
uses: actions/setup-node@v4
|
|
33
|
+
with:
|
|
34
|
+
node-version: 22
|
|
35
|
+
cache: npm
|
|
36
|
+
|
|
37
|
+
- name: Install Dependencies
|
|
38
|
+
run: npm ci
|
|
39
|
+
|
|
40
|
+
- name: Mutate the whole source tree
|
|
41
|
+
id: watch
|
|
42
|
+
env:
|
|
43
|
+
MUTATION_WATCH_REPORT: ${{ runner.temp }}/mutation-watch-report.md
|
|
44
|
+
run: node scripts/mutation-gate.mjs weekly
|
|
45
|
+
|
|
46
|
+
- name: Keep the report
|
|
47
|
+
if: always()
|
|
48
|
+
uses: actions/upload-artifact@v4
|
|
49
|
+
with:
|
|
50
|
+
name: mutation-watch-report
|
|
51
|
+
path: |
|
|
52
|
+
reports/mutation/mutation.json
|
|
53
|
+
${{ runner.temp }}/mutation-watch-report.md
|
|
54
|
+
if-no-files-found: ignore
|
|
55
|
+
|
|
56
|
+
report:
|
|
57
|
+
needs: watch
|
|
58
|
+
# a failure files an issue; a cancelled run does so only when the schedule
|
|
59
|
+
# cancelled it (the 90-minute timeout), so cancelling a manual run by hand
|
|
60
|
+
# does not claim a regression that never happened
|
|
61
|
+
if: >-
|
|
62
|
+
always() &&
|
|
63
|
+
(needs.watch.result == 'failure' ||
|
|
64
|
+
(needs.watch.result == 'cancelled' && github.event_name == 'schedule'))
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
timeout-minutes: 5
|
|
67
|
+
permissions:
|
|
68
|
+
contents: read
|
|
69
|
+
issues: write
|
|
70
|
+
|
|
71
|
+
steps:
|
|
72
|
+
- name: File an issue
|
|
73
|
+
env:
|
|
74
|
+
GH_TOKEN: ${{ github.token }}
|
|
75
|
+
run: |
|
|
76
|
+
title='Mutation watch: a file carries more surviving mutants than the baseline'
|
|
77
|
+
existing=$(gh issue list --state open --search "\"$title\" in:title" \
|
|
78
|
+
--json number --jq '.[0].number // empty')
|
|
79
|
+
|
|
80
|
+
if [ -n "$existing" ]; then
|
|
81
|
+
echo "already tracked in #$existing"
|
|
82
|
+
exit 0
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
gh issue create --title "$title" --body \
|
|
86
|
+
"The weekly mutation run found a file carrying more surviving mutants than mutation-baseline.json records, so behaviour was merged without a test that fails on it.
|
|
87
|
+
|
|
88
|
+
The run names the file and each surviving mutant (file:line:column, mutator, replacement), and prints the refreshed baseline:
|
|
89
|
+
|
|
90
|
+
$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID
|
|
91
|
+
|
|
92
|
+
Close this once a spec kills them and mutation-baseline.json is updated from that run."
|
package/README.md
CHANGED
|
@@ -118,7 +118,15 @@ The default header is as follows:
|
|
|
118
118
|
**/
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
-
A blank line separates the `<h4> SAS Macros </h4>` section from the header content before it and from whatever follows the list.
|
|
121
|
+
A blank line separates the `<h4> SAS Macros </h4>` section from the header content before it and from whatever follows the list.
|
|
122
|
+
|
|
123
|
+
`<h4> SAS Macros </h4>` is the compile dependency list: `sasjs compile` reads it and inlines each macro as precode, so an entry is a file the build resolves. It is the only header list this linter reads, and every other `<h4>` list in the header (`SAS Includes`, `Binary Files`, `SAS Programs`, `Service Inputs`, `Service Outputs`, ...) is left entirely alone.
|
|
124
|
+
|
|
125
|
+
Each entry is the tag and one file token - `@li mf_trim.sas` - indented to the same width as the section tag; nothing may follow the token. The extension is dropped to give the macro name, so `@li mp_as` is read as `mp_as` too. A line carrying anything after the file token is not an entry: prose belongs on its own line, and a section that carries such a line is left as its author wrote it rather than being rewritten or having the line removed. The section is machine-maintained, so text after the token would be lost on the next rewrite anyway.
|
|
126
|
+
|
|
127
|
+
A tag one slip away from `@li` (`@il`, `@l1`, `@1i`) is read as an entry, so a typo cannot hide the declarations below it - and because such a line is read, nothing reports it, so the formatter writes every entry back as `@li <file>` at the section's indentation. The repair edits the entry lines in place, so it adds no blank lines and moves nothing else in the header.
|
|
128
|
+
|
|
129
|
+
A line that opens with any other tag - `@example`, `@param` - ends the list rather than being read or converted. The entries above it are still readable and still rewritable, so the formatter keeps adding and removing entries above that line and leaves the rest of the header alone.
|
|
122
130
|
|
|
123
131
|
If creating a new value, use `{lineEnding}` instead of `\n`, eg as follows:
|
|
124
132
|
|
|
@@ -362,10 +370,12 @@ This will highlight lines with trailing spaces. Trailing spaces serve no useful
|
|
|
362
370
|
|
|
363
371
|
### noUndeclaredMacros
|
|
364
372
|
|
|
365
|
-
SASjs programs declare the macros they use in the `<h4> SAS Macros </h4>` section of the header, so that the compiler can assemble the job before it runs. This rule reports a warning for each macro that a file invokes without declaring it - either by defining it in the file, or by listing it
|
|
373
|
+
SASjs programs declare the macros they use in the `<h4> SAS Macros </h4>` section of the header, so that the compiler can assemble the job before it runs. This rule reports a warning for each macro that a file invokes without declaring it - either by defining it in the file, or by listing it in the `<h4> SAS Macros </h4>` section. That is the only header list `sasjs compile` reads, so it is the only one that makes a macro available to the job; entries anywhere else in the header do not count.
|
|
366
374
|
|
|
367
375
|
The macros that ship with SAS (`%scan`, `%index`, `%sysfunc` and the other macro functions, along with the macro language keywords such as `%if`, `%then` and `%do`) are always considered declared, so they are never reported. That list is generated from the `macroStatements` and `macroFunctions` groups of `@sasjs/sas-language` (`npm run generate:macros`), so it tracks the SAS language data rather than being maintained here. Macros made available through the `SASAUTOS` system option are not visible to the linter and will be reported - list them in [`ignoredMacros`](#ignoredmacros), which carries `webout` by default.
|
|
368
376
|
|
|
377
|
+
An invocation written inside the argument of `%str` / `%nrstr` is not reported. A file masks a trigger to print it as often as to defer it, and nothing runs at that point, so a warning there would land on text that may never be code. [`noUnusedMacros`](#nounusedmacros) counts such a name as a use, so the deferred form - `%mp_abort(iftrue=%str(%mf_existds(work.x)=1))` - satisfies the declaration it depends on without demanding one.
|
|
378
|
+
|
|
369
379
|
The warning is resolved by adding the macro name to the header. Running `sasjs lint fix` (or saving in the SASjs VS Code extension with `formatOnSave`) adds the missing macros automatically, as a unique list sorted alphabetically.
|
|
370
380
|
|
|
371
381
|
- Default: true
|
|
@@ -421,7 +431,9 @@ The counterpart of `noUndeclaredMacros`. This rule reports a warning for each ma
|
|
|
421
431
|
|
|
422
432
|
A macro can be referenced indirectly - for example by another macro in the dependency chain, or through a dynamic `%¯o` call - without appearing as a direct invocation in the file, so the warning is advisory.
|
|
423
433
|
|
|
424
|
-
|
|
434
|
+
An invocation written inside the argument of `%str` / `%nrstr` counts as a use. That text is data while the masking function runs, but its caller normally unquotes it - `%mp_abort(iftrue=%str(%mf_existds(work.x)=1))` resolves `%mf_existds` when `%mp_abort` evaluates the condition - so the declaration is a dependency of the file, and treating it as unused would have the formatter delete it.
|
|
435
|
+
|
|
436
|
+
Running `sasjs lint fix` (or saving in the SASjs VS Code extension with `formatOnSave`) removes the unused entries from the header. Together with `noUndeclaredMacros`, this leaves the `<h4> SAS Macros </h4>` section listing exactly the macros the file uses. The pass that reads the list also writes each entry back as `@li <macro>.sas` at the section's indentation, in place and without re-spacing the header, so a mistyped tag (`@il`) is repaired even when no entry is removed.
|
|
425
437
|
|
|
426
438
|
- Default: true
|
|
427
439
|
- Severity: WARNING
|
package/jest.config.js
CHANGED
|
@@ -1,12 +1,61 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
preset: 'ts-jest',
|
|
3
3
|
testEnvironment: 'node',
|
|
4
|
+
// A Stryker run keeps a sandbox under .stryker-tmp with a copy of src/ in it.
|
|
5
|
+
// Without this, any jest run that overlaps a mutation run collects every spec
|
|
6
|
+
// twice (measured: 946 tests instead of 464) and reports failures from files
|
|
7
|
+
// that are being mutated. Anchored to <rootDir> on purpose: inside the sandbox
|
|
8
|
+
// the whole project IS under .stryker-tmp, and an unanchored pattern would make
|
|
9
|
+
// Stryker's own dry run collect nothing ("No tests were executed").
|
|
10
|
+
testPathIgnorePatterns: [
|
|
11
|
+
'/node_modules/',
|
|
12
|
+
'<rootDir>/.stryker-tmp/',
|
|
13
|
+
'<rootDir>/build/'
|
|
14
|
+
],
|
|
4
15
|
coverageThreshold: {
|
|
5
16
|
global: {
|
|
6
17
|
branches: 80,
|
|
7
18
|
functions: 80,
|
|
8
19
|
lines: 80,
|
|
9
20
|
statements: -10
|
|
21
|
+
},
|
|
22
|
+
// Per-path floors, so a change that hollows out one area cannot hide behind
|
|
23
|
+
// the aggregate. Every number is the coverage measured when this was written,
|
|
24
|
+
// rounded down, so the block is a ratchet: raise one when the area improves,
|
|
25
|
+
// never lower one to get a branch green. `statements` is a PERCENTAGE unless
|
|
26
|
+
// it is negative, where `-N` reads as "at most N uncovered statements" - so a
|
|
27
|
+
// plain `0` asserts nothing at all (0% coverage passes), which is why the two
|
|
28
|
+
// fully-covered areas ask for 100 and the three with known gaps carry a budget
|
|
29
|
+
// measured from the tree on the day this landed.
|
|
30
|
+
'./src/utils/': {
|
|
31
|
+
branches: 95,
|
|
32
|
+
functions: 100,
|
|
33
|
+
lines: 99,
|
|
34
|
+
statements: -4
|
|
35
|
+
},
|
|
36
|
+
'./src/rules/': {
|
|
37
|
+
branches: 95,
|
|
38
|
+
functions: 100,
|
|
39
|
+
lines: 100,
|
|
40
|
+
statements: 100
|
|
41
|
+
},
|
|
42
|
+
'./src/lint/': {
|
|
43
|
+
branches: 85,
|
|
44
|
+
functions: 100,
|
|
45
|
+
lines: 100,
|
|
46
|
+
statements: -2
|
|
47
|
+
},
|
|
48
|
+
'./src/format/': {
|
|
49
|
+
branches: 90,
|
|
50
|
+
functions: 100,
|
|
51
|
+
lines: 100,
|
|
52
|
+
statements: 100
|
|
53
|
+
},
|
|
54
|
+
'./src/types/': {
|
|
55
|
+
branches: 95,
|
|
56
|
+
functions: 100,
|
|
57
|
+
lines: 96,
|
|
58
|
+
statements: -4
|
|
10
59
|
}
|
|
11
60
|
},
|
|
12
61
|
collectCoverageFrom: ['src/**/{!(index|formatExample|lintExample),}.ts']
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest config for Stryker runs.
|
|
3
|
+
*
|
|
4
|
+
* The only difference from jest.config.js is that coverage is off: Stryker
|
|
5
|
+
* instruments the code itself, and the coverage thresholds would fail a
|
|
6
|
+
* single-mutant run. The suite itself runs in full, so the weekly baseline is
|
|
7
|
+
* built on every test - a skipped spec here would record mutants it covers as
|
|
8
|
+
* legitimate survivors and let them into the baseline permanently.
|
|
9
|
+
*/
|
|
10
|
+
const base = require('./jest.config.js')
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
...base,
|
|
14
|
+
collectCoverage: false,
|
|
15
|
+
coverageThreshold: undefined,
|
|
16
|
+
collectCoverageFrom: undefined
|
|
17
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Surviving-mutant counts per file, used by scripts/mutation-gate.mjs. Refresh from the output of npm run mutation:watch; raise a count only with a reason in the PR that raises it.",
|
|
3
|
+
"generatedAt": "2026-10-25T20:06:38Z",
|
|
4
|
+
"files": {
|
|
5
|
+
"src/rules/file/noUnusedMacros.ts": {
|
|
6
|
+
"score": 100.0,
|
|
7
|
+
"survived": 0,
|
|
8
|
+
"killed": 37,
|
|
9
|
+
"timeout": 0
|
|
10
|
+
},
|
|
11
|
+
"src/utils/getIndicesOf.ts": {
|
|
12
|
+
"score": 100.0,
|
|
13
|
+
"survived": 0,
|
|
14
|
+
"killed": 15,
|
|
15
|
+
"timeout": 6
|
|
16
|
+
},
|
|
17
|
+
"src/utils/isIgnored.ts": {
|
|
18
|
+
"score": 100.0,
|
|
19
|
+
"survived": 0,
|
|
20
|
+
"killed": 25,
|
|
21
|
+
"timeout": 0
|
|
22
|
+
},
|
|
23
|
+
"src/utils/parseMacroCalls.ts": {
|
|
24
|
+
"score": 92.98,
|
|
25
|
+
"survived": 25,
|
|
26
|
+
"killed": 293,
|
|
27
|
+
"timeout": 38
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
"publishInit": "cp -r ./build/* . && rm -rf ./build && rm -rf ./src && rm tsconfig.json",
|
|
13
13
|
"package:lib": "npm run build && cp ./package.json ./checkNodeVersion.js build && cp README.md build && cd build && npm version \"5.0.0\" && npm pack",
|
|
14
14
|
"lint:fix": "npx prettier --write \"{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
|
15
|
-
"lint": "npx prettier --check \"{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\""
|
|
15
|
+
"lint": "npx prettier --check \"{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
|
16
|
+
"mutation": "stryker run",
|
|
17
|
+
"mutation:changed": "node scripts/mutation-gate.mjs pr",
|
|
18
|
+
"mutation:watch": "node scripts/mutation-gate.mjs weekly"
|
|
16
19
|
},
|
|
17
20
|
"publishConfig": {
|
|
18
21
|
"access": "public"
|
|
@@ -62,6 +65,8 @@
|
|
|
62
65
|
"devDependencies": {
|
|
63
66
|
"@nogoo9/gitleaks": "8.30.1-post.2",
|
|
64
67
|
"@sasjs/sas-language": "0.3.0",
|
|
68
|
+
"@stryker-mutator/core": "10.1.1",
|
|
69
|
+
"@stryker-mutator/jest-runner": "10.1.1",
|
|
65
70
|
"@types/jest": "30.0.0",
|
|
66
71
|
"@types/node": "26.6.3",
|
|
67
72
|
"jest": "30.5.2",
|
|
@@ -73,5 +78,12 @@
|
|
|
73
78
|
"@sasjs/utils": "4.0.0",
|
|
74
79
|
"ignore": "7.0.11"
|
|
75
80
|
},
|
|
76
|
-
"version": "4.3.
|
|
81
|
+
"version": "4.3.2",
|
|
82
|
+
"devEngines": {
|
|
83
|
+
"runtime": {
|
|
84
|
+
"name": "node",
|
|
85
|
+
"version": ">=22.18",
|
|
86
|
+
"onFail": "warn"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
77
89
|
}
|
|
@@ -10,14 +10,21 @@ const parseMacros_1 = require("../../utils/parseMacros");
|
|
|
10
10
|
const sasMacros_1 = require("../../utils/sasMacros");
|
|
11
11
|
const name = 'noUndeclaredMacros';
|
|
12
12
|
const description = 'Enforce that every macro used in a file is declared.';
|
|
13
|
-
const message = `Macro '{macro}' is not declared - add it to the <h4> SAS Macros </h4>
|
|
13
|
+
const message = `Macro '{macro}' is not declared - add it to the <h4> SAS Macros </h4> section of the header, or list the macro in 'ignoredMacros'`;
|
|
14
14
|
/**
|
|
15
15
|
* Collects the first invocation of each macro that the file uses but does not
|
|
16
16
|
* declare. A macro is declared when it is defined in the file, or listed in the
|
|
17
|
-
* `<h4> SAS Macros </h4>`
|
|
17
|
+
* `<h4> SAS Macros </h4>` section of its header - the only header list that
|
|
18
|
+
* `sasjs compile` reads.
|
|
18
19
|
* Macros that ship with SAS are always considered declared, and so are the
|
|
19
20
|
* names the configuration lists in `ignoredMacros` - a macro such as `%webout`
|
|
20
21
|
* has no file to point at.
|
|
22
|
+
*
|
|
23
|
+
* The scan leaves out the argument of `%str` / `%nrstr`: a file that masks a
|
|
24
|
+
* trigger usually does so to print it, and reporting it would be a new warning
|
|
25
|
+
* on text that never runs at that point. `noUnusedMacros` counts those names,
|
|
26
|
+
* which is the conservative direction - a deferred invocation satisfies a
|
|
27
|
+
* declaration without ever demanding one.
|
|
21
28
|
*/
|
|
22
29
|
const getUndeclaredCalls = (value, config) => {
|
|
23
30
|
if (!value)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"noUndeclaredMacros.js","sourceRoot":"","sources":["../../../src/rules/file/noUndeclaredMacros.ts"],"names":[],"mappings":";;;AAAA,uCAAoD;AAEpD,2DAAuD;AACvD,mDAA+C;AAC/C,2DAA6E;AAC7E,iEAAwE;AACxE,yDAAqD;AACrD,qDAAkD;AAElD,MAAM,IAAI,GAAG,oBAAoB,CAAA;AACjC,MAAM,WAAW,GAAG,sDAAsD,CAAA;AAC1E,MAAM,OAAO,GAAG,
|
|
1
|
+
{"version":3,"file":"noUndeclaredMacros.js","sourceRoot":"","sources":["../../../src/rules/file/noUndeclaredMacros.ts"],"names":[],"mappings":"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"}
|
|
@@ -13,16 +13,20 @@ const message = `Macro '{macro}' is declared in the <h4> SAS Macros </h4> sectio
|
|
|
13
13
|
* Collects the macros declared in the `<h4> SAS Macros </h4>` section that the
|
|
14
14
|
* file itself never invokes. A macro that appears more than once is reported
|
|
15
15
|
* once, at its first entry.
|
|
16
|
+
*
|
|
17
|
+
* The scan includes the argument of `%str` / `%nrstr`, because that text is
|
|
18
|
+
* usually unquoted by its caller and the invocation runs: the guard idiom
|
|
19
|
+
* `%mp_abort(iftrue=%str(%mf_existds(work.x)=1))` resolves `%mf_existds` when
|
|
20
|
+
* `%mp_abort` evaluates the condition. Counting it as unused would report a
|
|
21
|
+
* declaration the file depends on - and the formatter would then delete it.
|
|
16
22
|
*/
|
|
17
23
|
const getUnusedMacros = (value, config) => {
|
|
18
24
|
if (!value)
|
|
19
25
|
return [];
|
|
20
|
-
const used = new Set((0, parseMacroCalls_1.parseMacroCalls)(value, config).map((call) => call.name.toLowerCase()));
|
|
26
|
+
const used = new Set((0, parseMacroCalls_1.parseMacroCalls)(value, config, { includeMasked: true }).map((call) => call.name.toLowerCase()));
|
|
21
27
|
const unused = [];
|
|
22
28
|
const reported = new Set();
|
|
23
|
-
(0, headerMacros_1.getHeaderMacros)(value, config)
|
|
24
|
-
.filter((macro) => macro.section === 'sasMacros')
|
|
25
|
-
.forEach((macro) => {
|
|
29
|
+
(0, headerMacros_1.getHeaderMacros)(value, config).forEach((macro) => {
|
|
26
30
|
const key = macro.name.toLowerCase();
|
|
27
31
|
if (reported.has(key))
|
|
28
32
|
return;
|
|
@@ -47,9 +51,13 @@ const test = (value, config) => {
|
|
|
47
51
|
const fix = (value, config) => {
|
|
48
52
|
const cfg = config || new types_1.LintConfig();
|
|
49
53
|
const macros = getUnusedMacros(value, cfg).map((macro) => macro.name);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
const removed = macros.length
|
|
55
|
+
? (0, headerMacros_1.removeMacrosFromHeader)(value, cfg, macros)
|
|
56
|
+
: value;
|
|
57
|
+
// The list is normalised even when nothing is removed: a mistyped `@li` tag
|
|
58
|
+
// or a stray indentation is read as a declaration, so no diagnostic points
|
|
59
|
+
// at it and no other pass would repair it.
|
|
60
|
+
return (0, headerMacros_1.repairHeaderList)(removed, cfg);
|
|
53
61
|
};
|
|
54
62
|
/**
|
|
55
63
|
* Lint rule that reports macros declared in the `<h4> SAS Macros </h4>` section
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"noUnusedMacros.js","sourceRoot":"","sources":["../../../src/rules/file/noUnusedMacros.ts"],"names":[],"mappings":";;;AAAA,uCAAoD;AAEpD,2DAAuD;AACvD,mDAA+C;AAC/C,
|
|
1
|
+
{"version":3,"file":"noUnusedMacros.js","sourceRoot":"","sources":["../../../src/rules/file/noUnusedMacros.ts"],"names":[],"mappings":";;;AAAA,uCAAoD;AAEpD,2DAAuD;AACvD,mDAA+C;AAC/C,2DAKiC;AACjC,iEAA6D;AAE7D,MAAM,IAAI,GAAG,gBAAgB,CAAA;AAC7B,MAAM,WAAW,GACf,yFAAyF,CAAA;AAC3F,MAAM,OAAO,GAAG,2FAA2F,CAAA;AAE3G;;;;;;;;;;GAUG;AACH,MAAM,eAAe,GAAG,CAAC,KAAa,EAAE,MAAkB,EAAiB,EAAE;IAC3E,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAA;IAErB,MAAM,IAAI,GAAG,IAAI,GAAG,CAClB,IAAA,iCAAe,EAAC,KAAK,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CACnE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CACxB,CACF,CAAA;IAED,MAAM,MAAM,GAAkB,EAAE,CAAA;IAChC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAA;IAElC,IAAA,8BAAe,EAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE;QAC/C,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAA;QACpC,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAM;QAC7B,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACjB,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAM;QAEzB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACpB,CAAC,CAAC,CAAA;IAEF,OAAO,MAAM,CAAA;AACf,CAAC,CAAA;AAED,MAAM,IAAI,GAAG,CAAC,KAAa,EAAE,MAAmB,EAAgB,EAAE;IAChE,MAAM,GAAG,GAAG,MAAM,IAAI,IAAI,kBAAU,EAAE,CAAA;IACtC,MAAM,QAAQ,GAAG,GAAG,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,mBAAQ,CAAC,OAAO,CAAA;IAE5D,OAAO,eAAe,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACjD,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC;QAC/C,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,iBAAiB,EAAE,KAAK,CAAC,iBAAiB;QAC1C,eAAe,EAAE,KAAK,CAAC,eAAe;QACtC,QAAQ;KACT,CAAC,CAAC,CAAA;AACL,CAAC,CAAA;AAED,MAAM,GAAG,GAAG,CAAC,KAAa,EAAE,MAAmB,EAAU,EAAE;IACzD,MAAM,GAAG,GAAG,MAAM,IAAI,IAAI,kBAAU,EAAE,CAAA;IACtC,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAErE,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM;QAC3B,CAAC,CAAC,IAAA,qCAAsB,EAAC,KAAK,EAAE,GAAG,EAAE,MAAM,CAAC;QAC5C,CAAC,CAAC,KAAK,CAAA;IAET,4EAA4E;IAC5E,2EAA2E;IAC3E,2CAA2C;IAC3C,OAAO,IAAA,+BAAgB,EAAC,OAAO,EAAE,GAAG,CAAC,CAAA;AACvC,CAAC,CAAA;AAED;;;;GAIG;AACU,QAAA,cAAc,GAAiB;IAC1C,IAAI,EAAE,2BAAY,CAAC,IAAI;IACvB,IAAI;IACJ,WAAW;IACX,OAAO;IACP,IAAI;IACJ,GAAG;CACJ,CAAA"}
|
package/sasjslint-schema.json
CHANGED
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
"$id": "#/properties/noUndeclaredMacros",
|
|
208
208
|
"type": "boolean",
|
|
209
209
|
"title": "noUndeclaredMacros",
|
|
210
|
-
"description": "Reports a warning for each macro a file invokes without declaring it in the `<h4> SAS Macros </h4>`
|
|
210
|
+
"description": "Reports a warning for each macro a file invokes without declaring it in the `<h4> SAS Macros </h4>` section of the header - the only header list `sasjs compile` reads. Macros that ship with SAS are always considered declared, and so are the macros listed in `ignoredMacros` (`webout` by default). The formatter resolves the warning by adding the macro names to the header, sorted alphabetically.",
|
|
211
211
|
"default": true,
|
|
212
212
|
"examples": [true, false]
|
|
213
213
|
},
|