Skip to content

Commit 9701b56

Browse files
Add support for AGLint inline config comments (#4)
* Add support for AGLint inline config comments * Remove unused packages Resolve #4 (comment)
1 parent 13e7876 commit 9701b56

File tree

8 files changed

+537
-40
lines changed

8 files changed

+537
-40
lines changed

package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,7 @@
8181
"typescript": "^4.8.4"
8282
},
8383
"dependencies": {
84-
"clone-deep": "^4.0.1",
8584
"css-tree": "^2.2.1",
86-
"globrex": "^0.1.2"
85+
"json5": "^2.2.1"
8786
}
8887
}

src/parser/comment/comment.ts

+12
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { IMetadata, MetadataParser } from "./metadata";
77
import { IPreProcessor, PreProcessorParser } from "./preprocessor";
88
import { RuleCategories } from "../common";
99
import { AdblockSyntax } from "../../utils/adblockers";
10+
import { ConfigCommentParser, IConfigComment } from "./inline-config";
1011

1112
/**
1213
* Represents a simple comment.
@@ -64,6 +65,14 @@ export interface ISimpleComment extends IComment {
6465
* ! Version: 2.0.150
6566
* ```
6667
* - etc.
68+
* - AGLint inline config comments:
69+
* - ```adblock
70+
* ! aglint-enable some-rule
71+
* ```
72+
* - ```adblock
73+
* ! aglint-disable some-rule
74+
* ```
75+
* - etc.
6776
* - Simple comments:
6877
* - ```adblock
6978
* ! This is just a comment
@@ -139,6 +148,7 @@ export class CommentParser {
139148
HintParser.parse(trimmed) ||
140149
PreProcessorParser.parse(trimmed) ||
141150
MetadataParser.parse(trimmed) ||
151+
ConfigCommentParser.parse(trimmed) ||
142152
<IComment>{
143153
category: RuleCategories.Comment,
144154
type: CommentRuleType.Comment,
@@ -165,6 +175,8 @@ export class CommentParser {
165175
return PreProcessorParser.generate(<IPreProcessor>ast);
166176
case CommentRuleType.Metadata:
167177
return MetadataParser.generate(<IMetadata>ast);
178+
case CommentRuleType.ConfigComment:
179+
return ConfigCommentParser.generate(<IConfigComment>ast);
168180
case CommentRuleType.Comment:
169181
return (<ISimpleComment>ast).marker + (<ISimpleComment>ast).text;
170182
}

src/parser/comment/common.ts

+9
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export enum CommentRuleType {
1313
Agent = "Agent",
1414
Hint = "Hint",
1515
PreProcessor = "PreProcessor",
16+
ConfigComment = "ConfigComment",
1617
}
1718

1819
/**
@@ -51,6 +52,14 @@ export enum CommentRuleType {
5152
* ! Version: 2.0.150
5253
* ```
5354
* - etc.
55+
* - AGLint inline config comments:
56+
* - ```adblock
57+
* ! aglint-enable some-rule
58+
* ```
59+
* - ```adblock
60+
* ! aglint-disable some-rule
61+
* ```
62+
* - etc.
5463
* - Simple comments:
5564
* - ```adblock
5665
* ! This is just a comment

src/parser/comment/inline-config.ts

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/**
2+
* AGLint configuration comments. Inspired by ESLint inline configuration comments:
3+
*
4+
* @see {@link https://eslint.org/docs/latest/user-guide/configuring/rules#using-configuration-comments}
5+
*/
6+
7+
import { AdblockSyntax } from "../../utils/adblockers";
8+
import { COMMA, EMPTY, SPACE } from "../../utils/constants";
9+
import { RuleCategories } from "../common";
10+
import { CommentMarker, CommentRuleType, IComment } from "./common";
11+
import JSON5 from "json5";
12+
13+
const AGLINT_COMMAND_PREFIX = "aglint";
14+
const PARAMS_SEPARATOR = COMMA;
15+
const CONFIG_COMMENT_MARKER = "--";
16+
17+
/**
18+
* Represents an inline configuration comment.
19+
*
20+
* For example, if the comment is
21+
* ```adblock
22+
* ! aglint-disable some-rule another-rule
23+
* ```
24+
* then the command is `aglint-disable` and its params is `["some-rule", "another-rule"]`.
25+
*/
26+
export interface IConfigComment extends IComment {
27+
category: RuleCategories.Comment;
28+
type: CommentRuleType.ConfigComment;
29+
30+
/** Comment marker: `!` or `#` */
31+
marker: CommentMarker;
32+
33+
/** Command, for example: `aglint` */
34+
command: string;
35+
36+
/** Params: can be a rule configuration object or a list of rule names */
37+
params?: object | string[];
38+
39+
/** Config comment, for example: `! aglint-enable -- this is the comment` */
40+
comment?: string;
41+
}
42+
43+
/**
44+
* ConfigCommentParser is responsible for parsing inline AGLint configuration rules.
45+
*
46+
* Inspired by ESLint inline configuration comments.
47+
*
48+
* @see {@link https://eslint.org/docs/latest/user-guide/configuring/rules#using-configuration-comments}
49+
*/
50+
export class ConfigCommentParser {
51+
/**
52+
* Determines whether the rule is an inline configuration comment rule.
53+
*
54+
* @param {string} raw - Raw rule
55+
* @returns {boolean} true/false
56+
*/
57+
public static isConfigComment(raw: string): boolean {
58+
const trimmed = raw.trim();
59+
60+
if (trimmed[0] == CommentMarker.Regular || trimmed[0] == CommentMarker.Hashmark) {
61+
// Skip comment marker and trim comment text (it is necessary because of "! something")
62+
const text = raw.slice(1).trim();
63+
64+
// The code below is "not pretty", but it runs fast, which is necessary, since it will run on EVERY comment
65+
// The essence of the indicator is that the control comment always starts with the "aglint" prefix
66+
return (
67+
(text[0] == "a" || text[0] == "A") &&
68+
(text[1] == "g" || text[1] == "G") &&
69+
(text[2] == "l" || text[2] == "L") &&
70+
(text[3] == "i" || text[3] == "I") &&
71+
(text[4] == "n" || text[4] == "N") &&
72+
(text[5] == "t" || text[5] == "T")
73+
);
74+
}
75+
76+
return false;
77+
}
78+
79+
/**
80+
* Parses a raw rule as an inline configuration comment.
81+
*
82+
* @param {string} raw - Raw rule
83+
* @returns {IConfigComment | null}
84+
* Inline configuration comment AST or null (if the raw rule cannot be parsed as configuration comment)
85+
*/
86+
public static parse(raw: string): IConfigComment | null {
87+
const trimmed = raw.trim();
88+
89+
if (!ConfigCommentParser.isConfigComment(trimmed)) {
90+
return null;
91+
}
92+
93+
let text = raw.slice(1).trim();
94+
let comment: string | undefined;
95+
96+
// Remove comment part, for example: "! aglint rule1: "off" -- this is a comment"
97+
// Correct rules doesn't includes "--" inside
98+
const commentPos = text.indexOf(CONFIG_COMMENT_MARKER);
99+
if (commentPos != -1) {
100+
comment = text.substring(commentPos + 2).trim();
101+
text = text.substring(0, commentPos).trim();
102+
}
103+
104+
// Prepare result
105+
const result: IConfigComment = {
106+
category: RuleCategories.Comment,
107+
type: CommentRuleType.ConfigComment,
108+
syntax: AdblockSyntax.Unknown,
109+
marker: raw[0] as CommentMarker,
110+
command: text,
111+
};
112+
113+
if (comment) {
114+
result.comment = comment;
115+
}
116+
117+
let rawParams: string | undefined;
118+
119+
// Get the AGLint command and its parameters. For example, if the following config comment is given:
120+
// ! aglint-disable something
121+
// then the command is "aglint-disable" and the parameter is "something".
122+
const firstSpaceIndex = text.indexOf(SPACE);
123+
if (firstSpaceIndex != -1) {
124+
result.command = text.substring(0, firstSpaceIndex).trim().toLocaleLowerCase();
125+
rawParams = text.substring(firstSpaceIndex + 1).trim();
126+
}
127+
128+
// If the command is simply "aglint", then it is a special case whose parameter is a rule configuration object
129+
if (result.command === AGLINT_COMMAND_PREFIX) {
130+
if (!rawParams || rawParams.length == 0) {
131+
throw new SyntaxError("Missing configuration object");
132+
}
133+
134+
// Delegate to JSON parser (JSON5 also supports unquoted properties)
135+
// TODO: Is some structure validation required at this point? This is currently just a general object
136+
result.params = JSON5.parse(`{ ${rawParams} }`);
137+
} else if (rawParams) {
138+
result.params = rawParams.split(PARAMS_SEPARATOR).map((param) => param.trim());
139+
}
140+
141+
return result;
142+
}
143+
144+
/**
145+
* Converts an inline configuration comment AST to a string.
146+
*
147+
* @param {IConfigComment} ast - Inline configuration comment AST
148+
* @returns {string} Raw string
149+
*/
150+
public static generate(ast: IConfigComment): string {
151+
let result = EMPTY;
152+
153+
result += ast.marker;
154+
result += SPACE;
155+
result += ast.command;
156+
157+
if (ast.params) {
158+
result += SPACE;
159+
if (Array.isArray(ast.params)) {
160+
result += ast.params.join(PARAMS_SEPARATOR + SPACE);
161+
} else {
162+
result += JSON.stringify(ast.params).slice(1, -1).trim();
163+
}
164+
}
165+
166+
if (ast.comment) {
167+
result += SPACE;
168+
result += CONFIG_COMMENT_MARKER;
169+
result += SPACE;
170+
result += ast.comment;
171+
}
172+
173+
return result;
174+
}
175+
}

test/parser/comment/comment.test.ts

+71
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,69 @@ describe("CommentParser", () => {
188188
value: "https://github.com/AdguardTeam/some-repo/wiki",
189189
});
190190

191+
// Config comments
192+
expect(CommentParser.parse("! aglint-disable rule1, rule2")).toEqual({
193+
category: RuleCategories.Comment,
194+
syntax: AdblockSyntax.Unknown,
195+
type: CommentRuleType.ConfigComment,
196+
marker: "!",
197+
command: "aglint-disable",
198+
params: ["rule1", "rule2"],
199+
});
200+
201+
expect(CommentParser.parse("! aglint-enable rule1, rule2")).toEqual({
202+
category: RuleCategories.Comment,
203+
syntax: AdblockSyntax.Unknown,
204+
type: CommentRuleType.ConfigComment,
205+
marker: "!",
206+
command: "aglint-enable",
207+
params: ["rule1", "rule2"],
208+
});
209+
210+
expect(CommentParser.parse("# aglint-disable rule1, rule2")).toEqual({
211+
category: RuleCategories.Comment,
212+
syntax: AdblockSyntax.Unknown,
213+
type: CommentRuleType.ConfigComment,
214+
marker: "#",
215+
command: "aglint-disable",
216+
params: ["rule1", "rule2"],
217+
});
218+
219+
expect(CommentParser.parse("# aglint-enable rule1, rule2")).toEqual({
220+
category: RuleCategories.Comment,
221+
syntax: AdblockSyntax.Unknown,
222+
type: CommentRuleType.ConfigComment,
223+
marker: "#",
224+
command: "aglint-enable",
225+
params: ["rule1", "rule2"],
226+
});
227+
228+
expect(CommentParser.parse('! aglint rule1: "off", rule2: ["a", "b"] -- this is a comment')).toEqual({
229+
category: RuleCategories.Comment,
230+
syntax: AdblockSyntax.Unknown,
231+
type: CommentRuleType.ConfigComment,
232+
marker: "!",
233+
command: "aglint",
234+
params: {
235+
rule1: "off",
236+
rule2: ["a", "b"],
237+
},
238+
comment: "this is a comment",
239+
});
240+
241+
expect(CommentParser.parse('# aglint rule1: "off", rule2: ["a", "b"] -- this is a comment')).toEqual({
242+
category: RuleCategories.Comment,
243+
syntax: AdblockSyntax.Unknown,
244+
type: CommentRuleType.ConfigComment,
245+
marker: "#",
246+
command: "aglint",
247+
params: {
248+
rule1: "off",
249+
rule2: ["a", "b"],
250+
},
251+
comment: "this is a comment",
252+
});
253+
191254
// Comments
192255
expect(CommentParser.parse("! This is just a comment")).toEqual({
193256
category: RuleCategories.Comment,
@@ -251,6 +314,14 @@ describe("CommentParser", () => {
251314
"! Homepage: https://github.com/AdguardTeam/some-repo/wiki"
252315
);
253316

317+
expect(parseAndGenerate("! aglint-enable rule1, rule2 -- comment")).toEqual(
318+
"! aglint-enable rule1, rule2 -- comment"
319+
);
320+
321+
expect(parseAndGenerate("# aglint-enable rule1, rule2 -- comment")).toEqual(
322+
"# aglint-enable rule1, rule2 -- comment"
323+
);
324+
254325
expect(parseAndGenerate("! This is just a comment")).toEqual("! This is just a comment");
255326
});
256327
});

0 commit comments

Comments
 (0)