@sasjs/lint 4.3.1 → 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 CHANGED
@@ -12,7 +12,11 @@
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",
19
+ "check:package": "node scripts/checkPackageContents.mjs"
16
20
  },
17
21
  "publishConfig": {
18
22
  "access": "public"
@@ -62,6 +66,8 @@
62
66
  "devDependencies": {
63
67
  "@nogoo9/gitleaks": "8.30.1-post.2",
64
68
  "@sasjs/sas-language": "0.3.0",
69
+ "@stryker-mutator/core": "10.1.1",
70
+ "@stryker-mutator/jest-runner": "10.1.1",
65
71
  "@types/jest": "30.0.0",
66
72
  "@types/node": "26.6.3",
67
73
  "jest": "30.5.2",
@@ -69,9 +75,34 @@
69
75
  "ts-jest": "29.4.14",
70
76
  "typescript": "6.0.3"
71
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
+ ],
72
96
  "dependencies": {
73
- "@sasjs/utils": "4.0.0",
97
+ "@sasjs/utils": "^4.0.0",
74
98
  "ignore": "7.0.11"
75
99
  },
76
- "version": "4.3.1"
100
+ "version": "4.3.3",
101
+ "devEngines": {
102
+ "runtime": {
103
+ "name": "node",
104
+ "version": ">=22.18",
105
+ "onFail": "warn"
106
+ }
107
+ }
77
108
  }
@@ -27,9 +27,24 @@ const isIgnored = (fPath, configuration) => __awaiter(void 0, void 0, void 0, fu
27
27
  const config = configuration || (yield (0, _1.getLintConfig)());
28
28
  const projectRoot = yield (0, _1.getProjectRoot)();
29
29
  const gitIgnoreFilePath = path_1.default.join(projectRoot, '.gitignore');
30
- const rootPath = projectRoot + path_1.default.sep;
31
- const relativePath = fPath.replace(rootPath, '');
32
- if (fPath === projectRoot)
30
+ // path.relative rather than stripping `projectRoot + sep` from the string: a
31
+ // separator or a normalisation the caller did not apply leaves the strip with
32
+ // nothing to remove, and the `ignore` package then receives an absolute path
33
+ // and rejects it with a RangeError. path.relative is lexical - a symlinked root
34
+ // still yields a `..`-prefixed path, which the guard below catches.
35
+ const relativePath = path_1.default.relative(projectRoot, fPath);
36
+ if (relativePath === '')
37
+ return false;
38
+ // On Windows, `path.relative` returns an absolute path when the two paths sit
39
+ // on different drives, which the two guards below cannot see; passing that on
40
+ // reaches `ignore` and throws the RangeError this function exists to avoid.
41
+ if (path_1.default.isAbsolute(relativePath))
42
+ return false;
43
+ // Nothing in a project's .gitignore can match a path outside it, and the
44
+ // `ignore` package rejects a path that escapes the root ("path should be a
45
+ // path.relative()d string"), so a caller passing one gets `false` rather than
46
+ // a RangeError.
47
+ if (relativePath === '..' || relativePath.startsWith(`..${path_1.default.sep}`))
33
48
  return false;
34
49
  let gitIgnoreFileContent = '';
35
50
  if (yield (0, file_1.fileExists)(gitIgnoreFilePath))
@@ -1 +1 @@
1
- {"version":3,"file":"isIgnored.js","sourceRoot":"","sources":["../../src/utils/isIgnored.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,4CAAwD;AACxD,gDAAuB;AACvB,oDAA2B;AAC3B,wBAAiD;AAGjD;;;;;GAKG;AACI,MAAM,SAAS,GAAG,CACvB,KAAa,EACb,aAA0B,EACR,EAAE;IACpB,MAAM,MAAM,GAAG,aAAa,IAAI,CAAC,MAAM,IAAA,gBAAa,GAAE,CAAC,CAAA;IACvD,MAAM,WAAW,GAAG,MAAM,IAAA,iBAAc,GAAE,CAAA;IAC1C,MAAM,iBAAiB,GAAG,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAA;IAC9D,MAAM,QAAQ,GAAG,WAAW,GAAG,cAAI,CAAC,GAAG,CAAA;IACvC,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;IAEhD,IAAI,KAAK,KAAK,WAAW;QAAE,OAAO,KAAK,CAAA;IAEvC,IAAI,oBAAoB,GAAG,EAAE,CAAA;IAE7B,IAAI,MAAM,IAAA,iBAAU,EAAC,iBAAiB,CAAC;QACrC,oBAAoB,GAAG,MAAM,IAAA,eAAQ,EAAC,iBAAiB,CAAC,CAAA;IAE1D,OAAO,IAAA,gBAAM,GAAE;SACZ,GAAG,CAAC,oBAAoB,CAAC;SACzB,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC;SACtB,OAAO,CAAC,YAAY,CAAC,CAAA;AAC1B,CAAC,CAAA,CAAA;AArBY,QAAA,SAAS,aAqBrB"}
1
+ {"version":3,"file":"isIgnored.js","sourceRoot":"","sources":["../../src/utils/isIgnored.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,4CAAwD;AACxD,gDAAuB;AACvB,oDAA2B;AAC3B,wBAAiD;AAGjD;;;;;GAKG;AACI,MAAM,SAAS,GAAG,CACvB,KAAa,EACb,aAA0B,EACR,EAAE;IACpB,MAAM,MAAM,GAAG,aAAa,IAAI,CAAC,MAAM,IAAA,gBAAa,GAAE,CAAC,CAAA;IACvD,MAAM,WAAW,GAAG,MAAM,IAAA,iBAAc,GAAE,CAAA;IAC1C,MAAM,iBAAiB,GAAG,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAA;IAE9D,6EAA6E;IAC7E,8EAA8E;IAC9E,6EAA6E;IAC7E,gFAAgF;IAChF,oEAAoE;IACpE,MAAM,YAAY,GAAG,cAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAC,CAAA;IAEtD,IAAI,YAAY,KAAK,EAAE;QAAE,OAAO,KAAK,CAAA;IAErC,8EAA8E;IAC9E,8EAA8E;IAC9E,4EAA4E;IAC5E,IAAI,cAAI,CAAC,UAAU,CAAC,YAAY,CAAC;QAAE,OAAO,KAAK,CAAA;IAE/C,yEAAyE;IACzE,2EAA2E;IAC3E,8EAA8E;IAC9E,gBAAgB;IAChB,IAAI,YAAY,KAAK,IAAI,IAAI,YAAY,CAAC,UAAU,CAAC,KAAK,cAAI,CAAC,GAAG,EAAE,CAAC;QACnE,OAAO,KAAK,CAAA;IAEd,IAAI,oBAAoB,GAAG,EAAE,CAAA;IAE7B,IAAI,MAAM,IAAA,iBAAU,EAAC,iBAAiB,CAAC;QACrC,oBAAoB,GAAG,MAAM,IAAA,eAAQ,EAAC,iBAAiB,CAAC,CAAA;IAE1D,OAAO,IAAA,gBAAM,GAAE;SACZ,GAAG,CAAC,oBAAoB,CAAC;SACzB,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC;SACtB,OAAO,CAAC,YAAY,CAAC,CAAA;AAC1B,CAAC,CAAA,CAAA;AAtCY,QAAA,SAAS,aAsCrB"}
@@ -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
- }
@@ -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
@@ -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
@@ -1,156 +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
- ## Regenerating the shipped macro list
96
-
97
- 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`.
98
-
99
- ## Code style
100
-
101
- 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.
102
-
103
- ## Git hooks
104
-
105
- The repo ships its own hooks in [`.git-hooks/`](../.git-hooks):
106
-
107
- - `pre-commit` - scans staged changes for secrets with gitleaks, and requires the `@nogoo9/gitleaks` devDependency to be installed
108
- - `commit-msg` - verifies the commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) standard
109
-
110
- 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:
111
-
112
- ```bash
113
- git config core.hooksPath ./.git-hooks
114
- ```
115
-
116
- 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.
117
-
118
- 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`.
119
-
120
- ## Commit messages and releases
121
-
122
- 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.
123
-
124
- 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.
125
-
126
- Mark a genuinely breaking change with `BREAKING CHANGE:` in the commit body, and name the config key that restores the old behaviour.
127
-
128
- ## The audit gate
129
-
130
- `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.
131
-
132
- 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.
133
-
134
- ## CI is the enforcement point
135
-
136
- 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.
137
-
138
- ## The release toolchain lives in the workflow
139
-
140
- 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.
141
-
142
- 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.
143
-
144
- ## What to work on
145
-
146
- 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.
147
-
148
- Fixes not yet implemented, roughly in order of usefulness:
149
-
150
- - Change tabs to spaces
151
- - Remove gremlin characters
152
- - Convert single asterisk comments to block comments
153
-
154
- Harder, and still under investigation: automatic indentation and code layout.
155
-
156
- 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.
@@ -1,7 +0,0 @@
1
- version: 2
2
- updates:
3
- - package-ecosystem: npm
4
- directory: '/'
5
- schedule:
6
- interval: monthly
7
- open-pull-requests-limit: 3
@@ -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,169 +0,0 @@
1
- # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
- # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3
-
4
- name: SASjs Lint Build and Publish
5
-
6
- on:
7
- push:
8
- branches:
9
- - main
10
- workflow_dispatch:
11
-
12
- # One release at a time. Both triggers publish, and the push trigger fires on
13
- # every release commit @semantic-release/git pushes back to main, so a
14
- # dispatched recovery run can interleave with a push-triggered one. Two
15
- # semantic-release processes racing the same tag and the same npm publish is a
16
- # new way to strand a release - the state the retry below exists to recover.
17
- # `cancel-in-progress: false` queues the second run rather than cancelling the
18
- # first, because cancelling a run midway through a release is itself a way to
19
- # leave a tag pushed and the release missing.
20
- concurrency:
21
- group: publish
22
- cancel-in-progress: false
23
-
24
- jobs:
25
- build:
26
- runs-on: ubuntu-latest
27
- permissions:
28
- contents: write
29
- id-token: write # required for npm provenance (trusted publisher)
30
- issues: write # optional: lets @semantic-release/github comment on issues
31
- pull-requests: write # optional: lets @semantic-release/github comment on PRs
32
-
33
- strategy:
34
- matrix:
35
- node-version: [22]
36
-
37
- steps:
38
- # Mint a short-lived GitHub App token (bypass-capable)
39
- - name: Generate token
40
- id: app-token
41
- uses: actions/create-github-app-token@v1
42
- with:
43
- app-id: ${{ secrets.APP_ID }}
44
- private-key: ${{ secrets.APP_PRIVATE_KEY }}
45
-
46
- # Checkout using the app token so the release commit can be pushed
47
- - uses: actions/checkout@v4
48
- with:
49
- token: ${{ steps.app-token.outputs.token }}
50
- persist-credentials: true
51
- fetch-depth: 0 # semantic-release needs full history + tags
52
-
53
- - name: Use Node.js ${{ matrix.node-version }}
54
- uses: actions/setup-node@v4
55
- with:
56
- node-version: ${{ matrix.node-version }}
57
- registry-url: https://registry.npmjs.org
58
-
59
- # Node 22 ships npm 10.x — OIDC trusted publishing needs npm >= 11.5.1
60
- - name: Update npm
61
- run: npm install -g npm@latest
62
-
63
- - name: Install Dependencies
64
- run: npm ci
65
-
66
- - name: Check Code Style
67
- run: npm run lint
68
-
69
- - name: Run Unit Tests
70
- run: npm test
71
-
72
- - name: Build Project
73
- run: npm run build
74
-
75
- - name: Clean up ready for publishing
76
- run: npm run publishInit
77
-
78
- - name: Semantic Release
79
- # The release toolchain is deliberately not a project dependency: every
80
- # @semantic-release plugin declares a `semantic-release` peer, which npm
81
- # auto-installs together with the `npm` package semantic-release bundles
82
- # - and that bundled npm carries advisories nothing here can fix.
83
- # Installing it here, pinned, keeps the dependency tree clean. Keep
84
- # these versions in step with `release.plugins` in package.json.
85
- #
86
- # semantic-release pushes twice: @semantic-release/git pushes the release
87
- # commit to the branch, then core pushes the tag in a second, tags-only
88
- # push. When that second push is rejected the branch keeps the release
89
- # commit - package.json at the new version - with no tag: no GitHub
90
- # release, no npm publish, and every later run dying in the exec plugin's
91
- # `npm version` with "Version not changed", a state only a hand repair
92
- # clears. sasjs/adapter hit exactly that on 2026-10-17.
93
- #
94
- # The tag push runs after the tag exists locally and after the branch is
95
- # updated, so one more attempt has only the tag to push. Retry exactly
96
- # that case: a local tag alone does not identify it, because the tag is
97
- # also there when the failure came later (the GitHub release), and
98
- # retrying that finds the tag restored by semantic-release's own fetch,
99
- # decides there is nothing to release, exits 0 and leaves the step green
100
- # with the release still missing - so require the tag to be absent from
101
- # the remote as well.
102
- #
103
- # Only a definitive answer counts. `git ls-remote --exit-code` exits 2 for
104
- # an absent ref but 128 when the query itself fails (network, auth,
105
- # unreachable remote), and treating every non-zero exit as "absent" would
106
- # retry on the transient GitHub-side failures that are the very class of
107
- # incident this retry repairs. So 2 retries, 0 and anything else fail the
108
- # step.
109
- run: |
110
- run_release() {
111
- npx --yes \
112
- --package semantic-release@25.0.9 \
113
- --package @semantic-release/exec@7.1.0 \
114
- --package @semantic-release/git@11.0.1 \
115
- semantic-release
116
- }
117
-
118
- if run_release; then
119
- exit 0
120
- fi
121
-
122
- version=$(node -p "require('./package.json').version")
123
-
124
- if ! git rev-parse -q --verify "refs/tags/v$version" >/dev/null; then
125
- echo "No local v$version tag, so the failure came before the tag was created - not retrying." >&2
126
- exit 1
127
- fi
128
-
129
- ls_rc=0
130
- git ls-remote --exit-code --tags origin "refs/tags/v$version" >/dev/null 2>&1 || ls_rc=$?
131
-
132
- if [ "$ls_rc" -eq 0 ]; then
133
- echo "v$version is already on the remote, so the failure came after the tag push - not retrying." >&2
134
- exit 1
135
- fi
136
-
137
- if [ "$ls_rc" -ne 2 ]; then
138
- echo "Could not establish whether v$version is on the remote (git ls-remote exited $ls_rc) - not retrying." >&2
139
- exit 1
140
- fi
141
-
142
- echo "Retrying: v$version exists locally but not on the remote, so the tag push is what failed."
143
- git tag -d "v$version"
144
- run_release
145
- env:
146
- GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} # <-- app token, not secrets.GITHUB_TOKEN
147
-
148
- # semantic-release only releases when it finds a releasable commit, and the
149
- # angular convention this repo is configured with does not treat `chore:` or
150
- # `docs:` as one. This step ran on `if: success()` regardless, so a merge with
151
- # no releasable commit published the version already in package.json, which
152
- # npm rejects with "You cannot publish over the previously published
153
- # versions". Compare against the registry instead, and publish only when
154
- # there is something new.
155
- - name: Publish to npm with trusted publisher
156
- id: publish
157
- if: success()
158
- run: |
159
- local_version=$(node -p "require('./package.json').version")
160
- published_version=$(npm view @sasjs/lint version 2>/dev/null || echo "")
161
-
162
- if [ "$local_version" = "$published_version" ]; then
163
- echo "released=false" >> "$GITHUB_OUTPUT"
164
- echo "Nothing to publish - $local_version is already on npm"
165
- exit 0
166
- fi
167
-
168
- npm publish --access public
169
- echo "released=true" >> "$GITHUB_OUTPUT"
package/.gitpod.yml DELETED
@@ -1,8 +0,0 @@
1
- # This configuration file was automatically generated by Gitpod.
2
- # Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file)
3
- # and commit this file to your remote git repository to share the goodness with others.
4
-
5
- tasks:
6
- - init: npm install && npm run build
7
-
8
-
@@ -1,16 +0,0 @@
1
- # Hooks for the pre-commit framework - https://pre-commit.com
2
- #
3
- # The repo also ships native hooks in .git-hooks/, covering the secret scan, a
4
- # 2MB commit size limit and Conventional Commits. But .npmrc sets
5
- # ignore-scripts=true, so the `prepare` script that would activate them never
6
- # runs on `npm i` - a fresh clone commits unchecked until the one-time command
7
- # in .github/CONTRIBUTING.md is run by hand.
8
- #
9
- # This file is the standard route instead: install the framework once with
10
- # `pre-commit install`, and the secret scan runs on every commit. The hook is
11
- # pinned to an exact gitleaks release tag.
12
- repos:
13
- - repo: https://github.com/gitleaks/gitleaks
14
- rev: v8.30.1
15
- hooks:
16
- - id: gitleaks
package/.prettierrc DELETED
@@ -1,6 +0,0 @@
1
- {
2
- "trailingComma": "none",
3
- "tabWidth": 2,
4
- "semi": false,
5
- "singleQuote": true
6
- }
package/.sasjslint DELETED
@@ -1,13 +0,0 @@
1
- {
2
- "noTrailingSpaces": true,
3
- "noEncodedPasswords": true,
4
- "hasDoxygenHeader": true,
5
- "noSpacesInFileNames": true,
6
- "maxLineLength": 80,
7
- "lowerCaseFileNames": true,
8
- "noTabIndentation": true,
9
- "indentationMultiple": 2,
10
- "hasMacroNameInMend": true,
11
- "noNestedMacros": true,
12
- "hasMacroParentheses": true
13
- }
package/jest.config.js DELETED
@@ -1,13 +0,0 @@
1
- module.exports = {
2
- preset: 'ts-jest',
3
- testEnvironment: 'node',
4
- coverageThreshold: {
5
- global: {
6
- branches: 80,
7
- functions: 80,
8
- lines: 80,
9
- statements: -10
10
- }
11
- },
12
- collectCoverageFrom: ['src/**/{!(index|formatExample|lintExample),}.ts']
13
- }
@@ -1,249 +0,0 @@
1
- /**
2
- * Watches the dev-tree audit exemption declared in `.github/workflows/build.yml`.
3
- *
4
- * That workflow gates on `npm audit --omit=dev` because the dev tree carries one
5
- * advisory nothing in this repo can fix: GHSA-hp3w-g68c-fv3c (sprintf-js,
6
- * reached through @jest/transform -> babel-plugin-istanbul ->
7
- * @istanbuljs/load-nyc-config -> js-yaml 3 -> argparse). Every package in that
8
- * chain is at its latest published version and the advisory lists no patched
9
- * release, so gating on it would mean a permanently red build.
10
- *
11
- * An exemption with no expiry is how a scoped gate becomes a permanent one, so
12
- * this script re-checks it weekly and reports when it is out of date:
13
- *
14
- * - a dev-tree advisory outside EXEMPTIONS appears - the exemption is too narrow
15
- * - an EXEMPTIONS advisory no longer appears in the tree - the exemption is dead
16
- * - a WATCHED_FOR_RELEASE package publishes a version newer than the one
17
- * recorded here - a fix may now exist, so re-evaluate and drop the exemption
18
- * - an EXEMPTIONS id is missing from the build.yml comment - the gate and this
19
- * script disagree about what is exempt
20
- *
21
- * Any of those exits 1, failing the scheduled run and making the workflow file an
22
- * issue. The known advisory on its own does NOT exit 1: it is expected, and a job
23
- * that is red every week trains people to ignore red.
24
- *
25
- * Run with `node scripts/audit-watch.mjs`.
26
- */
27
- import { execFileSync } from 'node:child_process'
28
- import { appendFileSync, readFileSync, writeFileSync } from 'node:fs'
29
- import path from 'node:path'
30
- import { fileURLToPath } from 'node:url'
31
-
32
- const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
33
- const BUILD_WORKFLOW = path.join('.github', 'workflows', 'build.yml')
34
-
35
- /**
36
- * Advisories the dev-scoped gate in build.yml is allowed to leave unaddressed.
37
- * Each one must also be named in that workflow's comment - the check below
38
- * enforces it, so the gate and this list cannot drift apart.
39
- */
40
- const EXEMPTIONS = ['GHSA-hp3w-g68c-fv3c']
41
-
42
- /**
43
- * Packages whose published version decides whether an exemption is still
44
- * justified. `newestKnown` is the newest release at the time the exemption was
45
- * written; a newer one on the registry means a fix may exist and the exemption
46
- * deserves a fresh look rather than another year of silence.
47
- */
48
- const WATCHED_FOR_RELEASE = [{ name: 'sprintf-js', newestKnown: '1.1.3' }]
49
-
50
- /**
51
- * `npm audit --json` exits 1 whenever it reports anything, so the exit code says
52
- * nothing about whether the command worked - parse the report instead.
53
- */
54
- const runAudit = () => {
55
- let stdout
56
- try {
57
- stdout = execFileSync('npm', ['audit', '--json'], {
58
- encoding: 'utf8',
59
- stdio: ['ignore', 'pipe', 'ignore']
60
- })
61
- } catch (error) {
62
- stdout = error.stdout
63
- }
64
- if (!stdout || !stdout.trim()) {
65
- throw new Error('npm audit produced no JSON - run `npm ci` first')
66
- }
67
- return JSON.parse(stdout)
68
- }
69
-
70
- /**
71
- * The advisories the report names, keyed by GHSA id. A `via` entry is a bare
72
- * package name when the advisory is inherited from a dependency, and an object
73
- * carrying the GHSA url when it is the root of the chain - only the latter are
74
- * advisories in their own right.
75
- */
76
- const collectAdvisories = (report) => {
77
- const advisories = new Map()
78
-
79
- for (const [packageName, info] of Object.entries(report?.vulnerabilities ?? {})) {
80
- for (const via of info?.via ?? []) {
81
- if (typeof via !== 'object' || via === null) continue
82
- const id = String(via.url ?? '').split('/').pop()
83
- if (!id?.startsWith('GHSA-')) continue
84
-
85
- const entry = advisories.get(id) ?? {
86
- severity: via.severity,
87
- title: via.title,
88
- packages: []
89
- }
90
- entry.packages.push(packageName)
91
- advisories.set(id, entry)
92
- }
93
- }
94
-
95
- return advisories
96
- }
97
-
98
- /** The version the registry currently resolves `latest` to, or null if offline. */
99
- const latestPublished = (name) => {
100
- try {
101
- return execFileSync('npm', ['view', name, 'version'], {
102
- encoding: 'utf8',
103
- stdio: ['ignore', 'pipe', 'ignore']
104
- }).trim()
105
- } catch {
106
- return null
107
- }
108
- }
109
-
110
- /**
111
- * Numeric dotted comparison, so `1.1.10` sorts above `1.1.9`. A prerelease
112
- * suffix is dropped from both sides first: `1.1.3-beta.1` is not newer than
113
- * `1.1.3`, and a prerelease that happens to be tagged `latest` must not raise
114
- * the alarm by itself.
115
- */
116
- const isNewer = (candidate, baseline) => {
117
- const a = candidate.split('-')[0].split('.')
118
- const b = baseline.split('-')[0].split('.')
119
- for (let i = 0; i < Math.max(a.length, b.length); i++) {
120
- const left = a[i] ?? '0'
121
- const right = b[i] ?? '0'
122
- if (left === right) continue
123
- const leftNumber = Number(left)
124
- const rightNumber = Number(right)
125
- if (Number.isNaN(leftNumber) || Number.isNaN(rightNumber)) {
126
- return left > right
127
- }
128
- return leftNumber > rightNumber
129
- }
130
- return false
131
- }
132
-
133
- /**
134
- * Write the report to the step summary and to the file the workflow reads.
135
- * Wrapped so a failure to report cannot mask the failure being reported - and
136
- * so the workflow's `gh issue create --body-file` always has a file, including
137
- * on the path below where the watch itself broke.
138
- */
139
- const writeReport = (summary) => {
140
- try {
141
- if (process.env.GITHUB_STEP_SUMMARY) {
142
- appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary)
143
- }
144
- if (process.env.AUDIT_WATCH_REPORT) {
145
- writeFileSync(process.env.AUDIT_WATCH_REPORT, summary)
146
- }
147
- } catch (error) {
148
- console.error(`could not write the report: ${error.message}`)
149
- }
150
- }
151
-
152
- const main = () => {
153
- const report = runAudit()
154
- const advisories = collectAdvisories(report)
155
- const problems = []
156
-
157
- for (const [id, advisory] of advisories) {
158
- if (EXEMPTIONS.includes(id)) continue
159
- problems.push(
160
- `new dev-tree advisory ${id} (${advisory.severity}): ${advisory.title} - ` +
161
- `packages: ${[...new Set(advisory.packages)].sort().join(', ')}. ` +
162
- 'Fix it, or add it to EXEMPTIONS and name it in the build.yml comment.'
163
- )
164
- }
165
-
166
- for (const id of EXEMPTIONS) {
167
- if (advisories.has(id)) continue
168
- problems.push(
169
- `exempted advisory ${id} no longer appears in the tree - the exemption is ` +
170
- 'dead weight. Remove it from EXEMPTIONS and from the build.yml comment, ' +
171
- 'and restore the full `npm audit` gate.'
172
- )
173
- }
174
-
175
- for (const { name, newestKnown } of WATCHED_FOR_RELEASE) {
176
- const latest = latestPublished(name)
177
- if (latest === null) {
178
- console.log(`skipped the registry check for ${name} - npm view failed`)
179
- continue
180
- }
181
- if (isNewer(latest, newestKnown)) {
182
- problems.push(
183
- `${name} has published ${latest}, newer than the ${newestKnown} recorded ` +
184
- 'with the exemption - a fix may exist. Re-evaluate the exemption, bump ' +
185
- 'the chain if it clears, and drop the exemption when it does.'
186
- )
187
- }
188
- }
189
-
190
- const buildWorkflow = readFileSync(path.join(ROOT, BUILD_WORKFLOW), 'utf8')
191
- for (const id of EXEMPTIONS) {
192
- if (buildWorkflow.includes(id)) continue
193
- problems.push(
194
- `exemption ${id} is not named in ${BUILD_WORKFLOW} - the gate and this ` +
195
- 'script disagree about what is exempt.'
196
- )
197
- }
198
-
199
- const { moderate = 0, high = 0, critical = 0, total = 0 } =
200
- report?.metadata?.vulnerabilities ?? {}
201
-
202
- const summary = [
203
- '## Dev-tree audit watch',
204
- '',
205
- `Full audit: ${total} advisories (${critical} critical, ${high} high, ${moderate} moderate).`,
206
- '',
207
- '| advisory | severity | packages | exempt |',
208
- '| --- | --- | --- | --- |',
209
- ...[...advisories].map(
210
- ([id, advisory]) =>
211
- `| ${id} | ${advisory.severity} | ${[...new Set(advisory.packages)].sort().join(', ')} | ` +
212
- `${EXEMPTIONS.includes(id) ? 'yes' : '**no**'} |`
213
- ),
214
- '',
215
- problems.length
216
- ? ['### The exemption needs attention', '', ...problems.map((p) => `- ${p}`), ''].join('\n')
217
- : 'The exemption still matches the tree - nothing to do.',
218
- ''
219
- ].join('\n')
220
-
221
- console.log(summary)
222
- writeReport(summary)
223
-
224
- process.exitCode = problems.length ? 1 : 0
225
- }
226
-
227
- try {
228
- main()
229
- } catch (error) {
230
- // The watch itself broke, so the exemption went unchecked. Say so in the
231
- // report rather than leaving the workflow to fail on a missing file.
232
- const summary = [
233
- '## Dev-tree audit watch',
234
- '',
235
- 'The watch did not complete, so the dev-tree audit exemption was **not**',
236
- 'checked against the tree. Nothing is known about the advisories right now.',
237
- '',
238
- '```',
239
- error.message,
240
- '```',
241
- '',
242
- 'Investigate the failing step, then re-run the workflow.',
243
- ''
244
- ].join('\n')
245
-
246
- console.error(`audit watch failed: ${error.message}`)
247
- writeReport(summary)
248
- process.exit(1)
249
- }
@@ -1,101 +0,0 @@
1
- /**
2
- * Generates `src/utils/sasMacros.generated.ts` from the SAS language data.
3
- *
4
- * The list of macros that ship with SAS is not lint's to maintain: it is
5
- * language data, and `@sasjs/sas-language` owns it. This script reads the
6
- * `macroStatements` and `macroFunctions` groups and emits the names the
7
- * `noUndeclaredMacros` rule treats as declared.
8
- *
9
- * The output is committed so that the published package carries no dependency
10
- * on the data package, and `src/utils/sasMacros.spec.ts` regenerates the list
11
- * from the same data and fails when the two differ, so a refresh upstream
12
- * cannot leave the constant stale.
13
- *
14
- * Run with `npm run generate:macros`.
15
- */
16
- import { readFile, writeFile } from 'node:fs/promises'
17
- import path from 'node:path'
18
- import { fileURLToPath } from 'node:url'
19
-
20
- const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
21
- const PACKAGE = '@sasjs/sas-language'
22
- const GROUPS = ['macroStatements', 'macroFunctions']
23
- const TARGET = path.join(ROOT, 'src', 'utils', 'sasMacros.generated.ts')
24
-
25
- const indexPath = (group) =>
26
- path.join(
27
- ROOT,
28
- 'node_modules',
29
- ...PACKAGE.split('/'),
30
- 'data',
31
- `${group}.index.json`
32
- )
33
-
34
- /**
35
- * Every macro name a group carries, as a bare lower-case name. Compound
36
- * entries such as `%DO %UNTIL` are dropped: they are a spelling of the
37
- * `%DO` statement and no source token can match them.
38
- */
39
- const collectMacroNames = async () => {
40
- const names = new Set()
41
-
42
- for (const group of GROUPS) {
43
- let parsed
44
- try {
45
- parsed = JSON.parse(await readFile(indexPath(group), 'utf8'))
46
- } catch (error) {
47
- throw new Error(
48
- `cannot read ${PACKAGE}/data/${group}.index.json - run 'npm install' first (${error.message})`
49
- )
50
- }
51
-
52
- if (!Array.isArray(parsed?.entries)) {
53
- throw new Error(
54
- `${PACKAGE}/data/${group}.index.json has an unexpected shape: expected { entries: [] }`
55
- )
56
- }
57
-
58
- for (const entry of parsed.entries) {
59
- const name = String(entry?.[0] ?? '')
60
- .replace(/^%/, '')
61
- .trim()
62
- .toLowerCase()
63
- if (!name || /\s/.test(name)) continue
64
- names.add(name)
65
- }
66
- }
67
-
68
- return [...names].sort()
69
- }
70
-
71
- /**
72
- * The generated file's text.
73
- */
74
- const render = (names) =>
75
- [
76
- '// Generated by scripts/generate-macro-names.mjs from @sasjs/sas-language.',
77
- '// Do not edit. Regenerate with `npm run generate:macros`.',
78
- '',
79
- '/**',
80
- ' * The macro names that ship with SAS, lower-cased and without the leading',
81
- ' * `%`, as published in the macroStatements and macroFunctions groups of',
82
- ' * `@sasjs/sas-language`.',
83
- ' */',
84
- 'export const sasMacros: readonly string[] = [',
85
- ...names.map((name) => ` '${name}',`),
86
- ']',
87
- ''
88
- ].join('\n')
89
-
90
- const main = async () => {
91
- const names = await collectMacroNames()
92
- await writeFile(TARGET, render(names))
93
- console.log(
94
- `written: src/utils/sasMacros.generated.ts (${names.length} names)`
95
- )
96
- }
97
-
98
- main().catch((error) => {
99
- console.error(`generation failed: ${error.message}`)
100
- process.exit(1)
101
- })