Skip to content

Commit 14af24f

Browse files
committed
AG-33771 Add config file schema generator
Squashed commit of the following: commit 83a062c Merge: 520df1b 6561059 Author: scripthunter7 <[email protected]> Date: Thu Nov 27 11:45:21 2025 +0100 Merge branch 'release/v4.0' into fix/AG-33771 commit 520df1b Merge: 9d67052 af7da8f Author: scripthunter7 <[email protected]> Date: Thu Nov 27 10:58:30 2025 +0100 Merge branch 'release/v4.0' into fix/AG-33771 commit 9d67052 Author: scripthunter7 <[email protected]> Date: Tue Nov 25 16:50:01 2025 +0100 refactor linterRuleTextSeveritySchema to use Object.keys() and add LinterRuleTextSeverity type - Added LinterRuleTextSeverity type as keyof typeof linterRuleSeverityMap - Modified linterRuleTextSeveritySchema to generate picklist from Object.keys(linterRuleSeverityMap) instead of hardcoded array - Updated normalizeSeverity() parameter type to use LinterRuleTextSeverity instead of string literals commit cd09c0a Merge: 4958c77 d122069 Author: scripthunter7 <[email protected]> Date: Mon Nov 24 21:01:13 2025 +0100 Merge branch 'release/v4.0' into fix/AG-33771 commit 4958c77 Author: scripthunter7 <[email protected]> Date: Mon Nov 24 20:15:31 2025 +0100 add unreleased section to changelog for JSON schema feature - Added Unreleased section with JSON schema for config files feature - Added link references for Unreleased comparison and issue #210 - Added missing version comparison link for 4.0.0-alpha.8 commit 0eb1c56 Author: scripthunter7 <[email protected]> Date: Mon Nov 24 20:11:18 2025 +0100 add support for yaml config files and .aglintrc without extension in schema fileMatch patterns - Extended VS Code settings.json fileMatch array to include aglint.config.yaml, aglint.config.yml, .aglintrc, .aglintrc.yaml, and .aglintrc.yml - Enables JSON schema validation and autocomplete for YAML config files and extensionless .aglintrc files commit 4c7d224 Author: scripthunter7 <[email protected]> Date: Mon Nov 24 20:09:09 2025 +0100 simplify schema descriptions to be more concise and remove redundant information about enum values and defaults - Shortened extends description to remove specific preset examples - Reduced syntax description to single line without listing all possible values or defaults - Simplified numeric and string severity descriptions to remove value enumerations - Updated schema/aglint-config.schema.json with matching simplified descriptions commit ae7c2ed Author: scripthunter7 <[email protected]> Date: Mon Nov 24 20:07:10 2025 +0100 refactor config schema generator to use async fs/promises API instead of sync methods - Changed fs import to use mkdir and writeFile from node:fs/promises - Modified main() function to be async and return Promise<void> - Replaced fs.existsSync() and fs.mkdirSync() with await mkdir() using recursive option - Replaced fs.writeFileSync() with await writeFile() - Removed unnecessary existence check since mkdir with recursive handles it commit ba17ec7 Author: scripthunter7 <[email protected]> Date: Mon Nov 24 20:05:41 2025 +0100 add descriptions to config file and rule schemas for better IDE autocomplete and documentation - Added description metadata to linterConfigFileSchema fields (root, extends, syntax, rules) - Added description metadata to rule severity schemas (numeric, text, and union variants) - Added description to linterRuleConfigSchema explaining severity and options format - Updated schema/aglint-config.schema.json with matching descriptions for all properties - Wrapped schema definitions in v.pipe() to attach commit e9cf033 Author: scripthunter7 <[email protected]> Date: Mon Nov 24 19:55:58 2025 +0100 Add config file schema generator - Removed valibot transform from linterRuleTextSeveritySchema to keep it as picklist - Added normalizeSeverity() helper function to convert 'off'/'warn'/'error' strings to LinterRuleSeverity enum values - Added normalizeConfig() method to ConfigResolver to normalize rule severity values in configs - Modified ConfigResolver.resolveChain() to normalize merged config before returning - Modifie
1 parent 6561059 commit 14af24f

10 files changed

Lines changed: 349 additions & 32 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
77
[Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
88
[Semantic Versioning]: https://semver.org/spec/v2.0.0.html
99

10+
## [Unreleased]
11+
12+
### Added
13+
14+
- JSON schema for config files. [#210]
15+
16+
[Unreleased]: https://github.com/AdguardTeam/AGLint/compare/v4.0.0-alpha.8...HEAD
17+
[#210]: https://github.com/AdguardTeam/AGLint/issues/210
18+
1019
## [4.0.0-alpha.8] - 2025-11-24
1120

1221
### Added
@@ -34,6 +43,8 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
3443

3544
- `getAglintRuleDocumentationUrl` from public exports.
3645

46+
[4.0.0-alpha.8]: https://github.com/AdguardTeam/AGLint/compare/v4.0.0-alpha.7...v4.0.0-alpha.8
47+
3748

3849
## [4.0.0-alpha.7] - 2025-11-19
3950

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@
142142
"files": [
143143
"dist/**",
144144
"!dist/build.txt",
145-
"config-presets/**"
145+
"config-presets/**",
146+
"schema/**"
146147
],
147148
"engines": {
148149
"node": ">=20"
@@ -159,7 +160,8 @@
159160
"prepare": "node .husky/install.js",
160161
"increment": "pnpm version patch --no-git-tag-version",
161162
"test": "vitest",
162-
"rules:update": "npx tsx tasks/generate-rule-exports.ts && npx tsx tasks/generate-rules-readme.ts && npx tsx tasks/generate-presets.ts"
163+
"rules:update": "npx tsx tasks/generate-rule-exports.ts && npx tsx tasks/generate-rules-readme.ts && npx tsx tasks/generate-presets.ts",
164+
"schema:generate": "npx tsx tasks/generate-config-schema.ts"
163165
},
164166
"devDependencies": {
165167
"@rollup/plugin-commonjs": "^25.0.2",

schema/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# AGLint JSON Schema
2+
3+
This folder contains JSON Schema files for AGLint configuration files.
4+
5+
## Files
6+
7+
- **aglint-config.schema.json** - JSON Schema for AGLint configuration files (aglint.config.json, .aglintrc.json, etc.)
8+
9+
## Usage
10+
11+
### Visual Studio Code
12+
13+
Add the schema to your configuration file to get autocompletion and validation:
14+
15+
```json
16+
{
17+
"$schema": "https://raw.githubusercontent.com/AdguardTeam/AGLint/main/schema/aglint-config.schema.json",
18+
"rules": {
19+
"no-short-rules": "error"
20+
}
21+
}
22+
```
23+
24+
Or configure it in your VS Code settings.json:
25+
26+
```json
27+
{
28+
"json.schemas": [
29+
{
30+
"fileMatch": [
31+
"aglint.config.json",
32+
"aglint.config.yaml",
33+
"aglint.config.yml",
34+
".aglintrc",
35+
".aglintrc.json",
36+
".aglintrc.yaml",
37+
".aglintrc.yml"
38+
],
39+
"url": "./node_modules/@adguard/aglint/schema/aglint-config.schema.json"
40+
}
41+
]
42+
}
43+
```
44+
45+
### Other IDEs
46+
47+
Most modern IDEs support JSON Schema validation. Refer to your IDE's documentation for how to associate JSON files
48+
with schemas.
49+
50+
## Generating the Schema
51+
52+
The schema is automatically generated from the Valibot schema defined in the source code.
53+
To regenerate it:
54+
55+
```bash
56+
pnpm schema:generate
57+
```
58+
59+
This ensures the schema always stays in sync with the actual configuration validation logic.

schema/aglint-config.schema.json

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$id": "https://github.com/AdguardTeam/AGLint/schema/aglint-config.schema.json",
4+
"title": "AGLint Configuration",
5+
"description": "AGLint configuration file schema for aglint.config.json, .aglintrc.json, or package.json",
6+
"type": "object",
7+
"properties": {
8+
"root": {
9+
"type": "boolean",
10+
"default": false,
11+
"description": "Indicates whether this configuration file is the root. When true, AGLint stops looking for configuration files in parent directories."
12+
},
13+
"extends": {
14+
"type": "array",
15+
"items": {
16+
"type": "string",
17+
"minLength": 1
18+
},
19+
"description": "List of configuration files or presets to extend from. Configurations are merged with later entries overriding earlier ones. Presets can be referenced with the \"aglint:\" prefix."
20+
},
21+
"syntax": {
22+
"type": "array",
23+
"items": {
24+
"enum": [
25+
"Common",
26+
"AdblockPlus",
27+
"UblockOrigin",
28+
"AdGuard"
29+
]
30+
},
31+
"description": "AdBlock syntax variants to support"
32+
},
33+
"rules": {
34+
"type": "object",
35+
"additionalProperties": {
36+
"anyOf": [
37+
{
38+
"anyOf": [
39+
{
40+
"enum": [
41+
0,
42+
1,
43+
2
44+
],
45+
"description": "Rule severity as numeric value"
46+
},
47+
{
48+
"enum": [
49+
"off",
50+
"warn",
51+
"error"
52+
],
53+
"description": "Rule severity as string"
54+
}
55+
],
56+
"description": "Rule severity level controlling whether the rule is disabled, reports warnings, or reports errors"
57+
},
58+
{
59+
"type": "array",
60+
"items": [
61+
{
62+
"anyOf": [
63+
{
64+
"enum": [
65+
0,
66+
1,
67+
2
68+
],
69+
"description": "Rule severity as numeric value"
70+
},
71+
{
72+
"enum": [
73+
"off",
74+
"warn",
75+
"error"
76+
],
77+
"description": "Rule severity as string"
78+
}
79+
],
80+
"description": "Rule severity level controlling whether the rule is disabled, reports warnings, or reports errors"
81+
}
82+
],
83+
"minItems": 1
84+
}
85+
],
86+
"description": "Rule configuration: either a severity level alone, or an array with severity and additional options"
87+
},
88+
"description": "Rule configurations as key-value pairs where keys are rule names and values are severity levels or arrays with severity and options."
89+
}
90+
},
91+
"required": []
92+
}

src/cli/config-file/config-file.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,36 @@ export enum LinterConfigFileFormat {
4444
PackageJson = 'package.json',
4545
}
4646

47-
export const linterConfigFileSchema = v.object({
48-
root: v.optional(v.boolean(), false),
49-
extends: v.optional(v.array(v.pipe(v.string(), v.minLength(1)))),
50-
syntax: v.optional(syntaxArraySchema),
51-
rules: v.optional(linterRulesConfigSchema),
52-
});
47+
export const linterConfigFileSchema = v.pipe(
48+
v.object({
49+
root: v.pipe(
50+
v.optional(v.boolean(), false),
51+
v.description(
52+
'Indicates whether this configuration file is the root. When true, AGLint stops looking for '
53+
+ 'configuration files in parent directories.',
54+
),
55+
),
56+
extends: v.pipe(
57+
v.optional(v.array(v.pipe(v.string(), v.minLength(1)))),
58+
v.description(
59+
'List of configuration files or presets to extend from. Configurations are merged with later '
60+
+ 'entries overriding earlier ones. Presets can be referenced with the "aglint:" prefix.',
61+
),
62+
),
63+
syntax: v.pipe(
64+
v.optional(syntaxArraySchema),
65+
v.description('AdBlock syntax variants to support'),
66+
),
67+
rules: v.pipe(
68+
v.optional(linterRulesConfigSchema),
69+
v.description(
70+
'Rule configurations as key-value pairs where keys are rule names and values are severity '
71+
+ 'levels or arrays with severity and options.',
72+
),
73+
),
74+
}),
75+
v.description('AGLint configuration file schema for aglint.config.json, .aglintrc.json, or package.json'),
76+
);
5377

5478
export type LinterConfigFile = v.InferInput<typeof linterConfigFileSchema>;
5579

src/cli/utils/config-resolver/config-resolver.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as v from 'valibot';
22
import { parse as parseYaml } from 'yaml';
33

44
import { type LinterConfig } from '../../../linter/config';
5+
import { normalizeSeverity } from '../../../linter/rule';
56
import { type ModuleDebug } from '../../../utils/debug';
67
import { deepMerge } from '../../../utils/deep-merge';
78
import {
@@ -156,7 +157,7 @@ export class ConfigResolver {
156157
if (this.debug) {
157158
this.debug.log(`Final merged config: ${JSON.stringify(merged)}`);
158159
}
159-
return merged;
160+
return ConfigResolver.normalizeConfig(merged);
160161
}
161162

162163
/**
@@ -270,9 +271,12 @@ export class ConfigResolver {
270271
this.debug.log(`Flattened config for ${absPath}: ${JSON.stringify(flattened)}`);
271272
}
272273

274+
// Normalize severity values before caching
275+
const normalized = ConfigResolver.normalizeConfig(flattened);
276+
273277
// Cache result
274278
const entry: ConfigCacheEntry = {
275-
config: flattened,
279+
config: normalized,
276280
isRoot: parsed.root === true,
277281
timestamp: Date.now(),
278282
};
@@ -283,6 +287,39 @@ export class ConfigResolver {
283287
return entry;
284288
}
285289

290+
/**
291+
* Normalizes rule severity values from strings to enum values.
292+
*
293+
* @param config The config to normalize.
294+
*
295+
* @returns The normalized config.
296+
*/
297+
private static normalizeConfig(config: LinterConfig): LinterConfig {
298+
if (!config.rules) {
299+
return config;
300+
}
301+
302+
const normalizedRules: typeof config.rules = {};
303+
304+
for (const [ruleName, ruleConfig] of Object.entries(config.rules)) {
305+
if (Array.isArray(ruleConfig)) {
306+
// Tuple format: [severity, ...options]
307+
normalizedRules[ruleName] = [
308+
normalizeSeverity(ruleConfig[0]),
309+
...ruleConfig.slice(1),
310+
] as any;
311+
} else {
312+
// Single severity
313+
normalizedRules[ruleName] = normalizeSeverity(ruleConfig);
314+
}
315+
}
316+
317+
return {
318+
...config,
319+
rules: normalizedRules,
320+
};
321+
}
322+
286323
/**
287324
* Parses config file content.
288325
*

src/linter/rule-registry/rule-instance.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
type LinterRuleSeverity,
2020
type LinterRuleType,
2121
type LinterRuleVisitors,
22+
normalizeSeverity,
2223
type WithMessages,
2324
} from '../rule';
2425

@@ -201,7 +202,7 @@ export class LinterRuleInstance {
201202
// [severity[, ...config]] case
202203

203204
// eslint-disable-next-line prefer-destructuring
204-
this.severity = parsedConfig[0];
205+
this.severity = normalizeSeverity(parsedConfig[0]);
205206

206207
// If there are more than one element in the array, it means that there are additional config options
207208
// In this case we need to validate them against the rule's config schema
@@ -232,7 +233,7 @@ export class LinterRuleInstance {
232233
}
233234
} else {
234235
// severity-only case
235-
this.severity = parsedConfig;
236+
this.severity = normalizeSeverity(parsedConfig);
236237

237238
if (this.rule.meta.configSchema) {
238239
this.config.length = 0;

0 commit comments

Comments
 (0)