hana-linter 0.1.1
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/workflows/npm-publish.yml +29 -0
- package/.prettierrc +7 -0
- package/CHANGELOG.md +14 -0
- package/README.md +286 -0
- package/dist/cli.js +104 -0
- package/dist/config.js +227 -0
- package/dist/content-lint.js +151 -0
- package/dist/files.js +73 -0
- package/dist/index.js +31 -0
- package/dist/lint.js +195 -0
- package/dist/report.js +28 -0
- package/dist/types/cli.js +2 -0
- package/dist/types/config.js +2 -0
- package/dist/types/issues.js +2 -0
- package/dist/types/rules.js +2 -0
- package/package.json +28 -0
- package/src/assets/.hana-linter.json +224 -0
- package/src/cli.ts +113 -0
- package/src/config.ts +305 -0
- package/src/content-lint.ts +187 -0
- package/src/files.ts +84 -0
- package/src/index.ts +37 -0
- package/src/lint.ts +222 -0
- package/src/report.ts +29 -0
- package/src/types/cli.ts +21 -0
- package/src/types/config.ts +20 -0
- package/src/types/issues.ts +9 -0
- package/src/types/rules.ts +140 -0
- package/tsconfig.json +45 -0
|
@@ -0,0 +1,29 @@
|
|
|
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://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
|
|
3
|
+
|
|
4
|
+
name: Publish NPM Package
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches:
|
|
9
|
+
- main
|
|
10
|
+
release:
|
|
11
|
+
types: [created]
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
id-token: write # required for OIDC
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
publish:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: '24'
|
|
25
|
+
registry-url: 'https://registry.npmjs.org'
|
|
26
|
+
- run: npm ci
|
|
27
|
+
- run: npm run build --if-present
|
|
28
|
+
- run: npm test
|
|
29
|
+
- run: npm publish
|
package/.prettierrc
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
### 0.1.1
|
|
8
|
+
|
|
9
|
+
- Added `hana-linter init` command to generate a default `.hana-linter.json` in the current project root
|
|
10
|
+
- Added `hana-linter init --force` option to overwrite an existing config file
|
|
11
|
+
|
|
12
|
+
### Initial Release
|
|
13
|
+
|
|
14
|
+
- Initial version with core linter functionality
|
package/README.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# hana-linter
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/hana-linter)
|
|
4
|
+
[](https://github.com/qualiture/hana-linter/actions/workflows/npm-publish.yml)
|
|
5
|
+
[](https://github.com/qualiture/hana-linter/blob/main/LICENSE)
|
|
6
|
+
[](https://nodejs.org/)
|
|
7
|
+
|
|
8
|
+
Regex-first naming lint for SAP HANA artifacts in CAP projects.
|
|
9
|
+
|
|
10
|
+
[NPM package](https://www.npmjs.com/package/hana-linter) • [Report issue](https://github.com/qualiture/hana-linter/issues) • [Releases](https://github.com/qualiture/hana-linter/releases)
|
|
11
|
+
|
|
12
|
+
Lint SAP HANA artifact names in CAP projects using configurable regex-based naming rules.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Teams often rely on naming conventions for HANA artifacts such as tables and views. When these conventions are enforced manually, drift is common and code reviews become noisy.
|
|
17
|
+
|
|
18
|
+
hana-linter helps you:
|
|
19
|
+
|
|
20
|
+
- enforce naming standards consistently
|
|
21
|
+
- catch violations early in local development and CI
|
|
22
|
+
- apply different rules per file extension
|
|
23
|
+
- keep naming policy in version control via a single config file
|
|
24
|
+
|
|
25
|
+
## How It Works
|
|
26
|
+
|
|
27
|
+
hana-linter reads a `.hana-linter.json` file and validates artifact file names against rules grouped by extension.
|
|
28
|
+
|
|
29
|
+
It supports two lint modes:
|
|
30
|
+
|
|
31
|
+
- Full scan mode: no file arguments, recursively scans `rootDir`
|
|
32
|
+
- File-list mode: pass file paths, only those files are validated
|
|
33
|
+
|
|
34
|
+
Rule groups per extension:
|
|
35
|
+
|
|
36
|
+
- `groups.all`: every rule must match (AND)
|
|
37
|
+
- `groups.any`: at least one rule must match (OR)
|
|
38
|
+
|
|
39
|
+
You can define `extension: "*"` as a shared rule set. Its rules are applied to every file extension and are merged with any extension-specific rule set.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
### Local (recommended for projects)
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install --save-dev hana-linter
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Run with:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx hana-linter
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Global
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install -g hana-linter
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Quick Start
|
|
62
|
+
|
|
63
|
+
1. Generate a default config in your project root:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
hana-linter init
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
2. Run the linter:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
hana-linter
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
3. If needed, regenerate and overwrite config:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
hana-linter init --force
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Commands
|
|
82
|
+
|
|
83
|
+
### `hana-linter`
|
|
84
|
+
|
|
85
|
+
Run lint using `.hana-linter.json` from the current working directory.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
hana-linter
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Lint specific files only:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
hana-linter db/src/T_CUSTOMER.hdbtable db/src/V_ACTIVE_USERS.hdbview
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Use a custom config path:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
hana-linter --config ./config/.hana-linter.json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### `hana-linter init`
|
|
104
|
+
|
|
105
|
+
Create `.hana-linter.json` in the current working directory from the bundled default template.
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
hana-linter init
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Overwrite existing config:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
hana-linter init --force
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Configuration
|
|
118
|
+
|
|
119
|
+
Create a `.hana-linter.json` file in your project root.
|
|
120
|
+
|
|
121
|
+
### Configuration Fields
|
|
122
|
+
|
|
123
|
+
- `rootDir` (string): directory to scan in full scan mode
|
|
124
|
+
- `ignoredDirectories` (string[]): folder names ignored during recursive traversal
|
|
125
|
+
- `extensionRuleSets` (array): rule definitions grouped by file extension
|
|
126
|
+
- `contentRuleSets` (optional array): naming rules for identifiers extracted from file contents (for example table fields and procedure/function parameters)
|
|
127
|
+
|
|
128
|
+
Each `extensionRuleSets` item contains:
|
|
129
|
+
|
|
130
|
+
- `extension` (string): target extension, for example `.hdbtable`; use `*` to target all extensions
|
|
131
|
+
- `folderName` (optional string): enforce that matching files are located in a folder with this name (at any depth under `rootDir`)
|
|
132
|
+
- `groups.all` (optional array): all rules must match
|
|
133
|
+
- `groups.any` (optional array): at least one rule must match
|
|
134
|
+
|
|
135
|
+
Each rule contains:
|
|
136
|
+
|
|
137
|
+
- `description` (string): readable rule label for output
|
|
138
|
+
- `pattern` (string): regex source (without `/` delimiters)
|
|
139
|
+
- `flags` (optional string): regex flags, for example `i`, `u`, `iu`
|
|
140
|
+
|
|
141
|
+
At least one of `groups.all` or `groups.any` must be present for each extension.
|
|
142
|
+
|
|
143
|
+
When `folderName` is omitted, no folder-location enforcement is applied.
|
|
144
|
+
|
|
145
|
+
Each `contentRuleSets` item contains:
|
|
146
|
+
|
|
147
|
+
- `extension` (string): target extension, for example `.hdbtable`; use `*` to target all extensions
|
|
148
|
+
- `target` (string): extracted identifier type to validate; one of `field`, `inputParameter`, `outputParameter`
|
|
149
|
+
- `groups.all` (optional array): all rules must match
|
|
150
|
+
- `groups.any` (optional array): at least one rule must match
|
|
151
|
+
|
|
152
|
+
Supported extractors in this version:
|
|
153
|
+
|
|
154
|
+
- `field`: `.hdbtable`
|
|
155
|
+
- `inputParameter`: `.hdbprocedure`, `.hdbfunction`
|
|
156
|
+
- `outputParameter`: `.hdbprocedure`, `.hdbfunction`
|
|
157
|
+
|
|
158
|
+
### Default Config Example
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"rootDir": "db",
|
|
163
|
+
"ignoredDirectories": ["node_modules", ".git", "gen"],
|
|
164
|
+
"extensionRuleSets": [
|
|
165
|
+
{
|
|
166
|
+
"extension": "*",
|
|
167
|
+
"groups": {
|
|
168
|
+
"all": [
|
|
169
|
+
{
|
|
170
|
+
"description": "Upper snake case only",
|
|
171
|
+
"pattern": "^[A-Z0-9]+(?:_[A-Z0-9]+)*$"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"description": "Max length 30",
|
|
175
|
+
"pattern": "^.{1,30}$",
|
|
176
|
+
"flags": "u"
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"extension": ".hdbtable",
|
|
183
|
+
"folderName": "tables",
|
|
184
|
+
"groups": {
|
|
185
|
+
"any": [
|
|
186
|
+
{
|
|
187
|
+
"description": "Prefix T_",
|
|
188
|
+
"pattern": "^T_.+"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"description": "Prefix TX_",
|
|
192
|
+
"pattern": "^TX_.+"
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"extension": ".hdbview",
|
|
199
|
+
"groups": {
|
|
200
|
+
"all": [
|
|
201
|
+
{
|
|
202
|
+
"description": "Starts with V_",
|
|
203
|
+
"pattern": "^V_.+"
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
],
|
|
209
|
+
"contentRuleSets": [
|
|
210
|
+
{
|
|
211
|
+
"extension": ".hdbtable",
|
|
212
|
+
"target": "field",
|
|
213
|
+
"groups": {
|
|
214
|
+
"all": [
|
|
215
|
+
{
|
|
216
|
+
"description": "Field names in uppercase snake case",
|
|
217
|
+
"pattern": "^[A-Z0-9]+(?:_[A-Z0-9]+)*$"
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"extension": ".hdbprocedure",
|
|
224
|
+
"target": "inputParameter",
|
|
225
|
+
"groups": {
|
|
226
|
+
"all": [
|
|
227
|
+
{
|
|
228
|
+
"description": "Input parameters prefixed with IP_",
|
|
229
|
+
"pattern": "^IP_[A-Z0-9_]+$"
|
|
230
|
+
}
|
|
231
|
+
]
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"extension": ".hdbprocedure",
|
|
236
|
+
"target": "outputParameter",
|
|
237
|
+
"groups": {
|
|
238
|
+
"all": [
|
|
239
|
+
{
|
|
240
|
+
"description": "Output parameters prefixed with OP_",
|
|
241
|
+
"pattern": "^OP_[A-Z0-9_]+$"
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Exit Codes
|
|
251
|
+
|
|
252
|
+
- `0`: lint passed or `init` completed successfully
|
|
253
|
+
- `1`: lint violations found or command failed
|
|
254
|
+
|
|
255
|
+
This makes the CLI suitable for CI pipelines.
|
|
256
|
+
|
|
257
|
+
## CI Example
|
|
258
|
+
|
|
259
|
+
```yaml
|
|
260
|
+
name: lint-hana-names
|
|
261
|
+
on: [push, pull_request]
|
|
262
|
+
|
|
263
|
+
jobs:
|
|
264
|
+
hana-lint:
|
|
265
|
+
runs-on: ubuntu-latest
|
|
266
|
+
steps:
|
|
267
|
+
- uses: actions/checkout@v4
|
|
268
|
+
- uses: actions/setup-node@v4
|
|
269
|
+
with:
|
|
270
|
+
node-version: 20
|
|
271
|
+
- run: npm ci
|
|
272
|
+
- run: npx hana-linter
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Requirements
|
|
276
|
+
|
|
277
|
+
- Node.js >= 14
|
|
278
|
+
- npm >= 7
|
|
279
|
+
|
|
280
|
+
## Contributing
|
|
281
|
+
|
|
282
|
+
Contributions are welcome. Please open an issue or submit a pull request.
|
|
283
|
+
|
|
284
|
+
## License
|
|
285
|
+
|
|
286
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseCliInput = parseCliInput;
|
|
7
|
+
exports.runInit = runInit;
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const DEFAULT_CONFIG_PATH = '.hana-linter.json';
|
|
11
|
+
/**
|
|
12
|
+
* Parse command-line arguments.
|
|
13
|
+
*
|
|
14
|
+
* Supported:
|
|
15
|
+
* - --config <path>
|
|
16
|
+
* - remaining arguments are treated as file paths
|
|
17
|
+
*
|
|
18
|
+
* @returns Parsed CLI input.
|
|
19
|
+
*/
|
|
20
|
+
function parseCliInput() {
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
if (args[0] === 'init') {
|
|
23
|
+
const initArgs = args.slice(1);
|
|
24
|
+
let force = false;
|
|
25
|
+
for (const value of initArgs) {
|
|
26
|
+
if (value === '--force') {
|
|
27
|
+
force = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Unknown argument for init: "${value}". Supported: --force`);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
command: 'init',
|
|
34
|
+
files: [],
|
|
35
|
+
configPath: DEFAULT_CONFIG_PATH,
|
|
36
|
+
force
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const files = [];
|
|
40
|
+
let configPath = DEFAULT_CONFIG_PATH;
|
|
41
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
42
|
+
const value = args[index];
|
|
43
|
+
if (value === '--config') {
|
|
44
|
+
const nextValue = args[index + 1];
|
|
45
|
+
if (!nextValue || nextValue.startsWith('--')) {
|
|
46
|
+
throw new Error('Missing value for --config');
|
|
47
|
+
}
|
|
48
|
+
configPath = nextValue;
|
|
49
|
+
index += 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
files.push(value);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
command: 'lint',
|
|
56
|
+
files: files.filter((value) => value.trim().length > 0),
|
|
57
|
+
configPath,
|
|
58
|
+
force: false
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Create .hana-linter.json in current working directory from bundled template.
|
|
63
|
+
*
|
|
64
|
+
* @param force - Overwrite existing file when true.
|
|
65
|
+
*/
|
|
66
|
+
async function runInit(force) {
|
|
67
|
+
const templatePath = await resolveTemplateConfigPath();
|
|
68
|
+
const targetPath = node_path_1.default.resolve(process.cwd(), DEFAULT_CONFIG_PATH);
|
|
69
|
+
try {
|
|
70
|
+
await node_fs_1.promises.copyFile(templatePath, targetPath, force ? 0 : node_fs_1.constants.COPYFILE_EXCL);
|
|
71
|
+
console.info(`✅ Created ${DEFAULT_CONFIG_PATH} at ${targetPath}`);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (!force && typeof error === 'object' && error !== null && 'code' in error && error.code === 'EEXIST') {
|
|
75
|
+
throw new Error(`${DEFAULT_CONFIG_PATH} already exists at ${targetPath}. Use "hana-linter init --force" to overwrite.`);
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the packaged default config template path.
|
|
82
|
+
*
|
|
83
|
+
* Supports both:
|
|
84
|
+
* - built artifact layout (dist/assets)
|
|
85
|
+
* - source layout (src/assets) during local development
|
|
86
|
+
*
|
|
87
|
+
* @returns Existing template path.
|
|
88
|
+
*/
|
|
89
|
+
async function resolveTemplateConfigPath() {
|
|
90
|
+
const candidatePaths = [
|
|
91
|
+
node_path_1.default.resolve(__dirname, 'assets', DEFAULT_CONFIG_PATH),
|
|
92
|
+
node_path_1.default.resolve(__dirname, '..', 'src', 'assets', DEFAULT_CONFIG_PATH)
|
|
93
|
+
];
|
|
94
|
+
for (const candidatePath of candidatePaths) {
|
|
95
|
+
try {
|
|
96
|
+
await node_fs_1.promises.access(candidatePath);
|
|
97
|
+
return candidatePath;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Try next candidate.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
throw new Error('Could not locate bundled default configuration template. Expected one of: ' + candidatePaths.join(', '));
|
|
104
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readJsonConfig = readJsonConfig;
|
|
4
|
+
exports.toLintConfig = toLintConfig;
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
/**
|
|
7
|
+
* Read and parse JSON config file.
|
|
8
|
+
*
|
|
9
|
+
* @param configPath - Path to JSON configuration.
|
|
10
|
+
* @returns Parsed JSON config object.
|
|
11
|
+
*/
|
|
12
|
+
async function readJsonConfig(configPath) {
|
|
13
|
+
const rawContent = await node_fs_1.promises.readFile(configPath, { encoding: 'utf-8' });
|
|
14
|
+
const parsed = JSON.parse(rawContent);
|
|
15
|
+
if (!isJsonLintConfig(parsed)) {
|
|
16
|
+
throw new Error(`Invalid configuration schema in "${configPath}". Please verify required fields.`);
|
|
17
|
+
}
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Convert JSON config into runtime compiled config.
|
|
22
|
+
*
|
|
23
|
+
* @param jsonConfig - Parsed JSON config.
|
|
24
|
+
* @returns Runtime lint config with compiled regex.
|
|
25
|
+
*/
|
|
26
|
+
function toLintConfig(jsonConfig) {
|
|
27
|
+
const extensionFolderNames = new Map();
|
|
28
|
+
const extensionRuleSets = jsonConfig.extensionRuleSets.map((jsonRuleSet) => {
|
|
29
|
+
const folderName = jsonRuleSet.folderName?.trim();
|
|
30
|
+
if (folderName !== undefined && folderName.length === 0) {
|
|
31
|
+
throw new Error(`Invalid folderName for extension "${jsonRuleSet.extension}": value must not be empty.`);
|
|
32
|
+
}
|
|
33
|
+
if (folderName !== undefined && (folderName.includes('/') || folderName.includes('\\'))) {
|
|
34
|
+
throw new Error(`Invalid folderName for extension "${jsonRuleSet.extension}": use only a folder name, not a path.`);
|
|
35
|
+
}
|
|
36
|
+
if (folderName !== undefined) {
|
|
37
|
+
const existingFolderName = extensionFolderNames.get(jsonRuleSet.extension);
|
|
38
|
+
if (existingFolderName && existingFolderName !== folderName) {
|
|
39
|
+
throw new Error(`Conflicting folderName values configured for extension "${jsonRuleSet.extension}": "${existingFolderName}" and "${folderName}".`);
|
|
40
|
+
}
|
|
41
|
+
extensionFolderNames.set(jsonRuleSet.extension, folderName);
|
|
42
|
+
}
|
|
43
|
+
const compiledGroups = compileRuleGroup(jsonRuleSet.groups, `extension "${jsonRuleSet.extension}"`);
|
|
44
|
+
const allRules = compiledGroups.all ?? [];
|
|
45
|
+
const anyRules = compiledGroups.any ?? [];
|
|
46
|
+
const hasAllRules = allRules.length > 0;
|
|
47
|
+
const hasAnyRules = anyRules.length > 0;
|
|
48
|
+
if (!hasAllRules && !hasAnyRules) {
|
|
49
|
+
throw new Error(`Invalid rule configuration for extension "${jsonRuleSet.extension}": at least one rule is required in "groups.all" or "groups.any".`);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
extension: jsonRuleSet.extension,
|
|
53
|
+
folderName,
|
|
54
|
+
groups: compiledGroups
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
const contentRuleSets = (jsonConfig.contentRuleSets ?? []).map((jsonRuleSet) => {
|
|
58
|
+
const compiledGroups = compileRuleGroup(jsonRuleSet.groups, `content target "${jsonRuleSet.target}" extension "${jsonRuleSet.extension}"`);
|
|
59
|
+
const hasAllRules = (compiledGroups.all ?? []).length > 0;
|
|
60
|
+
const hasAnyRules = (compiledGroups.any ?? []).length > 0;
|
|
61
|
+
if (!hasAllRules && !hasAnyRules) {
|
|
62
|
+
throw new Error(`Invalid content rule configuration for target "${jsonRuleSet.target}" extension "${jsonRuleSet.extension}": at least one rule is required in "groups.all" or "groups.any".`);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
extension: jsonRuleSet.extension,
|
|
66
|
+
target: jsonRuleSet.target,
|
|
67
|
+
groups: compiledGroups
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
rootDir: jsonConfig.rootDir,
|
|
72
|
+
ignoredDirectories: jsonConfig.ignoredDirectories,
|
|
73
|
+
extensionRuleSets,
|
|
74
|
+
contentRuleSets
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Basic runtime schema guard for JSON config.
|
|
79
|
+
*
|
|
80
|
+
* @param value - Unknown parsed JSON value.
|
|
81
|
+
* @returns True when value matches expected shape.
|
|
82
|
+
*/
|
|
83
|
+
function isJsonLintConfig(value) {
|
|
84
|
+
if (!value || typeof value !== 'object') {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const candidate = value;
|
|
88
|
+
if (typeof candidate.rootDir !== 'string') {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (!Array.isArray(candidate.ignoredDirectories)) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (!Array.isArray(candidate.extensionRuleSets)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (candidate.contentRuleSets !== undefined && !Array.isArray(candidate.contentRuleSets)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const extensionRuleSetsValid = candidate.extensionRuleSets.every((ruleSet) => {
|
|
101
|
+
if (!ruleSet || typeof ruleSet !== 'object') {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const typedRuleSet = ruleSet;
|
|
105
|
+
if (typeof typedRuleSet.extension !== 'string') {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (typedRuleSet.folderName !== undefined && typeof typedRuleSet.folderName !== 'string') {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
if (!typedRuleSet.groups || typeof typedRuleSet.groups !== 'object') {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
const groups = typedRuleSet.groups;
|
|
115
|
+
const allRulesValid = groups.all === undefined ||
|
|
116
|
+
(Array.isArray(groups.all) &&
|
|
117
|
+
groups.all.every((rule) => !!rule &&
|
|
118
|
+
typeof rule.description === 'string' &&
|
|
119
|
+
typeof rule.pattern === 'string' &&
|
|
120
|
+
(rule.flags === undefined || typeof rule.flags === 'string')));
|
|
121
|
+
const anyRulesValid = groups.any === undefined ||
|
|
122
|
+
(Array.isArray(groups.any) &&
|
|
123
|
+
groups.any.every((rule) => !!rule &&
|
|
124
|
+
typeof rule.description === 'string' &&
|
|
125
|
+
typeof rule.pattern === 'string' &&
|
|
126
|
+
(rule.flags === undefined || typeof rule.flags === 'string')));
|
|
127
|
+
return allRulesValid && anyRulesValid;
|
|
128
|
+
});
|
|
129
|
+
if (!extensionRuleSetsValid) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return (candidate.contentRuleSets ?? []).every((ruleSet) => {
|
|
133
|
+
if (!ruleSet || typeof ruleSet !== 'object') {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const typedRuleSet = ruleSet;
|
|
137
|
+
if (typeof typedRuleSet.extension !== 'string') {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (!isContentTarget(typedRuleSet.target)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if (!typedRuleSet.groups || typeof typedRuleSet.groups !== 'object') {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
const groups = typedRuleSet.groups;
|
|
147
|
+
const allRulesValid = groups.all === undefined ||
|
|
148
|
+
(Array.isArray(groups.all) &&
|
|
149
|
+
groups.all.every((rule) => !!rule &&
|
|
150
|
+
typeof rule.description === 'string' &&
|
|
151
|
+
typeof rule.pattern === 'string' &&
|
|
152
|
+
(rule.flags === undefined || typeof rule.flags === 'string')));
|
|
153
|
+
const anyRulesValid = groups.any === undefined ||
|
|
154
|
+
(Array.isArray(groups.any) &&
|
|
155
|
+
groups.any.every((rule) => !!rule &&
|
|
156
|
+
typeof rule.description === 'string' &&
|
|
157
|
+
typeof rule.pattern === 'string' &&
|
|
158
|
+
(rule.flags === undefined || typeof rule.flags === 'string')));
|
|
159
|
+
return allRulesValid && anyRulesValid;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function isContentTarget(target) {
|
|
163
|
+
return target === 'field' || target === 'inputParameter' || target === 'outputParameter';
|
|
164
|
+
}
|
|
165
|
+
function compileRuleGroup(groups, contextRoot) {
|
|
166
|
+
const allRules = (groups.all ?? []).map((rule, index) => {
|
|
167
|
+
const flags = rule.flags ?? '';
|
|
168
|
+
const context = `${contextRoot} group "all" rule #${index + 1}`;
|
|
169
|
+
return {
|
|
170
|
+
description: rule.description,
|
|
171
|
+
source: rule.pattern,
|
|
172
|
+
flags,
|
|
173
|
+
pattern: compileRegex(rule.pattern, flags, context)
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
const anyRules = (groups.any ?? []).map((rule, index) => {
|
|
177
|
+
const flags = rule.flags ?? '';
|
|
178
|
+
const context = `${contextRoot} group "any" rule #${index + 1}`;
|
|
179
|
+
return {
|
|
180
|
+
description: rule.description,
|
|
181
|
+
source: rule.pattern,
|
|
182
|
+
flags,
|
|
183
|
+
pattern: compileRegex(rule.pattern, flags, context)
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
all: allRules.length > 0 ? allRules : undefined,
|
|
188
|
+
any: anyRules.length > 0 ? anyRules : undefined
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Validate regex flags for duplicate/unsupported values.
|
|
193
|
+
*
|
|
194
|
+
* @param flags - Raw regex flags string.
|
|
195
|
+
* @param context - Rule context for clear error messages.
|
|
196
|
+
*/
|
|
197
|
+
function validateRegexFlags(flags, context) {
|
|
198
|
+
const allowedFlags = new Set(['d', 'g', 'i', 'm', 's', 'u', 'v', 'y']);
|
|
199
|
+
const seen = new Set();
|
|
200
|
+
for (const flag of flags) {
|
|
201
|
+
if (!allowedFlags.has(flag)) {
|
|
202
|
+
throw new Error(`Invalid regex flag in ${context}: "${flag}"`);
|
|
203
|
+
}
|
|
204
|
+
if (seen.has(flag)) {
|
|
205
|
+
throw new Error(`Duplicate regex flag in ${context}: "${flag}"`);
|
|
206
|
+
}
|
|
207
|
+
seen.add(flag);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Compile regex source and flags into RegExp with helpful error context.
|
|
212
|
+
*
|
|
213
|
+
* @param source - Regex source from config.
|
|
214
|
+
* @param flags - Regex flags from config.
|
|
215
|
+
* @param context - Rule context for error reporting.
|
|
216
|
+
* @returns Compiled RegExp.
|
|
217
|
+
*/
|
|
218
|
+
function compileRegex(source, flags, context) {
|
|
219
|
+
validateRegexFlags(flags, context);
|
|
220
|
+
try {
|
|
221
|
+
return new RegExp(source, flags);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
const message = error instanceof Error ? error.message : 'Unknown regex error';
|
|
225
|
+
throw new Error(`Invalid regex in ${context}: pattern="${source}" flags="${flags}". ${message}`, { cause: error });
|
|
226
|
+
}
|
|
227
|
+
}
|