@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.
@@ -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. The same blank-line convention applies to the `<h4> Other Macros </h4>` section.
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 under `<h4> SAS Macros </h4>` or `<h4> Other Macros </h4>`.
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 `%&macro` call - without appearing as a direct invocation in the file, so the warning is advisory.
423
433
 
424
- 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.
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.0"
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> or <h4> Other Macros </h4> section of the header, or list the macro in 'ignoredMacros'`;
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>` or `<h4> Other Macros </h4>` section of its header.
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":"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"}
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
- if (!macros.length)
51
- return value;
52
- return (0, headerMacros_1.removeMacrosFromHeader)(value, cfg, macros);
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,2DAIiC;AACjC,iEAA6D;AAE7D,MAAM,IAAI,GAAG,gBAAgB,CAAA;AAC7B,MAAM,WAAW,GACf,yFAAyF,CAAA;AAC3F,MAAM,OAAO,GAAG,2FAA2F,CAAA;AAE3G;;;;GAIG;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,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CACtE,CAAA;IAED,MAAM,MAAM,GAAkB,EAAE,CAAA;IAChC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAA;IAElC,IAAA,8BAAe,EAAC,KAAK,EAAE,MAAM,CAAC;SAC3B,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,KAAK,WAAW,CAAC;SAChD,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE;QACjB,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;IAEJ,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,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IAEhC,OAAO,IAAA,qCAAsB,EAAC,KAAK,EAAE,GAAG,EAAE,MAAM,CAAC,CAAA;AACnD,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"}
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"}
@@ -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>` or `<h4> Other Macros </h4>` section of the header. 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.",
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
  },