Skip to content

Commit fd20d12

Browse files
authored
feat(directives): Add ui5lint-disable directives for XML, YAML and HTML (#623)
Follow up of #327 Resolves #419 JIRA: CPOUI5FOUNDATION-959
1 parent 25871d7 commit fd20d12

File tree

18 files changed

+452
-70
lines changed

18 files changed

+452
-70
lines changed

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,8 @@ UI5 linter supports directives similar to ESLint's configuration comments, allow
331331

332332
### Specifying Rules
333333

334+
**JavaScript / TypeScript**
335+
334336
You can disable specific rules by listing them after the directive. Rules must be separated by commas if several are given:
335337

336338
* `/* ui5lint-disable no-deprecated-api */`
@@ -341,9 +343,23 @@ An explanation why a rule is disabled can be added after the rule name; it must
341343

342344
* `// ui5lint-disable-next-line no-deprecated-api -- explanation`
343345

346+
**XML / HTML**
347+
348+
* `<!-- ui5lint-disable no-deprecated-api -->`
349+
* `<!-- ui5lint-disable-next-line -->`
350+
351+
Note that in XML and HTML files, depending on your browser runtime, the use of double-hyphen `--` is usually not allowed, making it impossible to add explanations as part of the directive. Please use separate comment blocks for this purpose.
352+
353+
Also, since comments can't be placed inside XML and HTML tags, you might need to use `ui5lint-disable` and `ui5lint-enable` directives outside of the tags instead of `ui5lint-disable-line` or `ui5lint-disable-next-line`.
354+
355+
**YAML**
356+
357+
* `# ui5lint-disable no-deprecated-api`
358+
* `# ui5lint-disable-next-line`
359+
344360
### Scope
345361

346-
Directives are currently supported in JavaScript and TypeScript files only; they are **not** supported in XML, YAML, HTML, or any other type of file.
362+
Directives are currently supported in JavaScript and TypeScript files as well as XML, HTML and YAML files.
347363

348364
## Node.js API
349365

src/linter/LinterContext.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {createReader} from "@ui5/fs/resourceFactory";
33
import {resolveLinks} from "../formatter/lib/resolveLinks.js";
44
import {LintMessageSeverity, MESSAGE, MESSAGE_INFO} from "./messages.js";
55
import {MessageArgs} from "./MessageArgs.js";
6-
import {Directive} from "./ui5Types/directives.js";
76
import ts from "typescript";
87

98
export type FilePattern = string; // glob patterns
@@ -100,6 +99,16 @@ export interface PositionRange {
10099
end?: PositionInfo;
101100
}
102101

102+
export type DirectiveAction = "enable" | "disable";
103+
export type DirectiveScope = "line" | "next-line" | undefined;
104+
export interface Directive {
105+
action: DirectiveAction;
106+
scope: DirectiveScope;
107+
ruleNames: string[];
108+
line: number;
109+
column: number;
110+
}
111+
103112
export interface LintMetadata {
104113
// The metadata holds information to be shared across linters
105114
directives: Set<Directive>;
@@ -216,6 +225,14 @@ export default class LinterContext {
216225
this.getCoverageInfo(resourcePath).push(coverageInfo);
217226
}
218227

228+
addDirective(resourcePath: ResourcePath, directive: Directive) {
229+
const metadata = this.getMetadata(resourcePath);
230+
if (!metadata.directives) {
231+
metadata.directives = new Set<Directive>();
232+
}
233+
metadata.directives.add(directive);
234+
}
235+
219236
#getMessageFromRawMessage<M extends MESSAGE>(rawMessage: RawLintMessage<M>): LintMessage {
220237
const messageInfo = MESSAGE_INFO[rawMessage.id];
221238
if (!messageInfo) {

src/linter/html/parser.ts

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,62 @@
11
import type {ReadStream} from "node:fs";
2-
import {SaxEventType, Tag as SaxTag} from "sax-wasm";
3-
import {parseXML} from "../../utils/xmlParser.js";
2+
import {SaxEventType, Tag as SaxTag, Text as SaxText} from "sax-wasm";
3+
import {extractDirective, parseXML} from "../../utils/xmlParser.js";
4+
import {Directive} from "../LinterContext.js";
45

56
interface ExtractedTags {
67
scriptTags: SaxTag[];
78
stylesheetLinkTags: SaxTag[];
89
}
10+
function parseTag(event: SaxEventType, tag: SaxTag, extractedTags: ExtractedTags) {
11+
if (event === SaxEventType.OpenTag &&
12+
tag.name === "link") {
13+
if (tag.attributes.some((attr) => {
14+
return (attr.name.value === "rel" &&
15+
attr.value.value === "stylesheet");
16+
})) {
17+
extractedTags.stylesheetLinkTags.push(tag);
18+
};
19+
} else if (event === SaxEventType.CloseTag &&
20+
tag.name === "script") {
21+
const isJSScriptTag = tag.attributes.every((attr) => {
22+
// The "type" attribute of the script tag should be
23+
// 1. not set (default),
24+
// 2. an empty string,
25+
// 3. or a JavaScript MIME type (text/javascript)
26+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
27+
return attr.name.value !== "type" ||
28+
(attr.name.value === "type" &&
29+
["",
30+
"module",
31+
"text/javascript",
32+
"application/javascript", /* legacy */
33+
].includes(attr.value.value.toLowerCase()));
34+
});
35+
if (isJSScriptTag) {
36+
extractedTags.scriptTags.push(tag);
37+
}
38+
}
39+
}
40+
41+
function parseComment(comment: SaxText, directives: Set<Directive>) {
42+
const directive = extractDirective(comment);
43+
if (directive) {
44+
directives.add(directive);
45+
}
46+
}
947

1048
export async function extractHTMLTags(contentStream: ReadStream) {
1149
const extractedTags: ExtractedTags = {
1250
scriptTags: [],
1351
stylesheetLinkTags: [],
1452
};
53+
const directives = new Set<Directive>();
1554
await parseXML(contentStream, (event, tag) => {
16-
if (!(tag instanceof SaxTag)) {
17-
return;
18-
}
19-
const serializedTag = tag.toJSON() as SaxTag;
20-
if (event === SaxEventType.OpenTag &&
21-
serializedTag.name === "link") {
22-
if (serializedTag.attributes.some((attr) => {
23-
return (attr.name.value === "rel" &&
24-
attr.value.value === "stylesheet");
25-
})) {
26-
extractedTags.stylesheetLinkTags.push(serializedTag);
27-
};
28-
} else if (event === SaxEventType.CloseTag &&
29-
serializedTag.name === "script") {
30-
const isJSScriptTag = serializedTag.attributes.every((attr) => {
31-
// The "type" attribute of the script tag should be
32-
// 1. not set (default),
33-
// 2. an empty string,
34-
// 3. or a JavaScript MIME type (text/javascript)
35-
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
36-
return attr.name.value !== "type" ||
37-
(attr.name.value === "type" &&
38-
["",
39-
"module",
40-
"text/javascript",
41-
"application/javascript", /* legacy */
42-
].includes(attr.value.value.toLowerCase()));
43-
});
44-
if (isJSScriptTag) {
45-
extractedTags.scriptTags.push(serializedTag);
46-
}
55+
if (tag instanceof SaxTag) {
56+
parseTag(event, tag.toJSON() as SaxTag, extractedTags);
57+
} else if (tag instanceof SaxText && event === SaxEventType.Comment) {
58+
parseComment(tag.toJSON() as SaxText, directives);
4759
}
4860
});
49-
return extractedTags;
61+
return {extractedTags, directives};
5062
}

src/linter/html/transpiler.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ export default async function transpileHtml(
1313
try {
1414
const taskEnd = taskStart("Transpile HTML", resourcePath, true);
1515
const report = new HtmlReporter(resourcePath, context);
16-
const {scriptTags, stylesheetLinkTags} = await extractHTMLTags(contentStream);
16+
const {extractedTags, directives} = await extractHTMLTags(contentStream);
1717

18+
if (directives.size) {
19+
context.getMetadata(resourcePath).directives = directives;
20+
}
21+
22+
const {scriptTags, stylesheetLinkTags} = extractedTags;
1823
const bootstrapTag = findBootstrapTag(scriptTags);
1924

2025
if (bootstrapTag) {

src/linter/ui5Types/directives.ts

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import ts from "typescript";
2-
import {LintMetadata} from "../LinterContext.js";
2+
import {Directive, DirectiveAction, DirectiveScope, LintMetadata} from "../LinterContext.js";
3+
4+
interface PossibleDirective extends Directive {
5+
pos: number;
6+
length: number;
7+
}
38

49
export function findDirectives(sourceFile: ts.SourceFile, metadata: LintMetadata) {
5-
metadata.directives = new Set<Directive>();
10+
metadata.directives ??= new Set<Directive>();
611

712
const possibleDirectives = collectPossibleDirectives(sourceFile);
813
if (possibleDirectives.size === 0) {
@@ -14,7 +19,7 @@ export function findDirectives(sourceFile: ts.SourceFile, metadata: LintMetadata
1419
}
1520

1621
function traverseAndFindDirectives(
17-
node: ts.Node, sourceText: string, possibleDirectives: Set<Directive>, confirmedDirectives: Set<Directive>
22+
node: ts.Node, sourceText: string, possibleDirectives: Set<PossibleDirective>, confirmedDirectives: Set<Directive>
1823
) {
1924
findDirectivesAroundNode(node, sourceText, possibleDirectives, confirmedDirectives);
2025
node.getChildren().forEach((child) => {
@@ -23,7 +28,7 @@ function traverseAndFindDirectives(
2328
}
2429

2530
function findDirectivesAroundNode(
26-
node: ts.Node, sourceText: string, possibleDirectives: Set<Directive>, confirmedDirectives: Set<Directive>
31+
node: ts.Node, sourceText: string, possibleDirectives: Set<PossibleDirective>, confirmedDirectives: Set<Directive>
2732
) {
2833
/*
2934
// This is a comment
@@ -87,29 +92,17 @@ function findDirectivesAroundNode(
8792
it would be impossible to know whether the code in the second line is part of the directive's description or not.
8893
*/
8994
/* eslint-disable max-len */
90-
const directiveRegex =
91-
/* | ----------------------------------------------- Multi-line comments -------------------------------------------- | ------------------------------------------ Single-line comments ------------------------------------| */
95+
const DIRECTIVE_REGEX =
96+
/* | ----------------------------------------------- Multi-line comments ----------------------------------------------- | ------------------------------------------ Single-line comments ----------------------------------------------------------------- | */
9297
/\/\*\s*ui5lint-(enable|disable)(?:-((?:next-)?line))?(\s+(?:[\w-]+\s*,\s*)*(?:\s*[\w-]+))?\s*,?\s*(?:--[\s\S]*?)?\*\/|\/\/\s*ui5lint-(enable|disable)(?:-((?:next-)?line))?([ \t]+(?:[\w-]+[ \t]*,[ \t]*)*(?:[ \t]*[\w-]+))?[ \t]*,?[ \t]*(?:--.*)?$/mg;
93-
/* |CG #1: action | | CG #2: scope | CG #3: rules |Dangling,| Description | |CG #4: action | | CG #5: scope | CG #6: rules |Dangling,| Description | */
98+
/* |CG #1: action | | CG #2: scope | CG #3: rules |Dangling,| Description | |CG #4: action | | CG #5: scope | CG #6: rules |Dangling,| Description | */
9499
/* eslint-enable max-len */
95100

96-
export type DirectiveAction = "enable" | "disable";
97-
export type DirectiveScope = "line" | "next-line" | undefined;
98-
export interface Directive {
99-
action: DirectiveAction;
100-
scope: DirectiveScope;
101-
ruleNames: string[];
102-
pos: number;
103-
length: number;
104-
line: number;
105-
column: number;
106-
}
107-
108101
export function collectPossibleDirectives(sourceFile: ts.SourceFile) {
109102
const text = sourceFile.getFullText();
110-
let match;
111-
const comments = new Set<Directive>();
112-
while ((match = directiveRegex.exec(text)) !== null) {
103+
const comments = new Set<PossibleDirective>();
104+
const matches = text.matchAll(DIRECTIVE_REGEX);
105+
for (const match of matches) {
113106
const action = (match[1] ?? match[4]) as DirectiveAction;
114107
const scope = (match[2] ?? match[5]) as DirectiveScope;
115108
const rules = match[3] ?? match[6];

src/linter/xmlTemplate/Parser.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@ import he from "he";
33
import ViewGenerator from "./generator/ViewGenerator.js";
44
import FragmentGenerator from "./generator/FragmentGenerator.js";
55
import JSTokenizer from "./lib/JSTokenizer.js";
6-
import LinterContext, {PositionInfo} from "../LinterContext.js";
6+
import LinterContext, {Directive, PositionInfo} from "../LinterContext.js";
77
import {TranspileResult} from "../LinterContext.js";
88
import AbstractGenerator from "./generator/AbstractGenerator.js";
99
import {getLogger} from "@ui5/logger";
1010
import {MESSAGE} from "../messages.js";
1111
import {ApiExtract} from "../../utils/ApiExtract.js";
1212
import ControllerByIdInfo from "./ControllerByIdInfo.js";
1313
import BindingLinter from "../binding/BindingLinter.js";
14-
import {Tag as SaxTag} from "sax-wasm";
14+
import {Tag as SaxTag, Text as SaxText} from "sax-wasm";
1515
import EventHandlerResolver from "./lib/EventHandlerResolver.js";
1616
import BindingParser from "../binding/lib/BindingParser.js";
17+
import {extractDirective} from "../../utils/xmlParser.js";
1718
const log = getLogger("linter:xmlTemplate:Parser");
1819

1920
export type Namespace = string;
@@ -132,6 +133,7 @@ export default class Parser {
132133
#xmlDocumentKind: DocumentKind;
133134

134135
#context: LinterContext;
136+
#directives = new Set<Directive>();
135137
#namespaceStack: NamespaceStackEntry[] = [];
136138
#nodeStack: NodeDeclaration[] = [];
137139

@@ -187,7 +189,18 @@ export default class Parser {
187189
this._removeNamespacesForLevel(level);
188190
}
189191

192+
parseComment(comment: SaxText) {
193+
const directive = extractDirective(comment);
194+
if (directive) {
195+
this.#directives.add(directive);
196+
}
197+
}
198+
190199
generate(): TranspileResult {
200+
if (this.#directives.size) {
201+
// Add directives to the context
202+
this.#context.getMetadata(this.#resourcePath).directives = this.#directives;
203+
}
191204
const {source, map} = this.#generator.getModuleContent();
192205
return {
193206
source,

src/linter/xmlTemplate/transpiler.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {SaxEventType, SAXParser, Tag as SaxTag} from "sax-wasm";
1+
import {SaxEventType, SAXParser, Tag as SaxTag, Text as SaxText} from "sax-wasm";
22
import {ReadStream} from "node:fs";
33
import fs from "node:fs/promises";
44
import {finished} from "node:stream/promises";
@@ -62,15 +62,17 @@ async function transpileXmlToJs(
6262
const parser = new Parser(resourcePath, apiExtract, context, controllerByIdInfo);
6363

6464
// Initialize parser
65-
const saxParser = new SAXParser(SaxEventType.OpenTag | SaxEventType.CloseTag);
65+
const saxParser = new SAXParser(SaxEventType.OpenTag | SaxEventType.CloseTag | SaxEventType.Comment);
6666

67-
saxParser.eventHandler = (event, tag) => {
68-
if (tag instanceof SaxTag) {
67+
saxParser.eventHandler = (event, data) => {
68+
if (data instanceof SaxTag) {
6969
if (event === SaxEventType.OpenTag) {
70-
parser.pushTag(tag.toJSON() as SaxTag);
70+
parser.pushTag(data.toJSON() as SaxTag);
7171
} else if (event === SaxEventType.CloseTag) {
72-
parser.popTag(tag.toJSON() as SaxTag);
72+
parser.popTag(data.toJSON() as SaxTag);
7373
}
74+
} else if (data instanceof SaxText && event === SaxEventType.Comment) {
75+
parser.parseComment(data.toJSON() as SaxText);
7476
}
7577
};
7678

src/linter/yaml/YamlLinter.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import LinterContext from "../LinterContext.js";
1+
import LinterContext, {DirectiveAction, DirectiveScope} from "../LinterContext.js";
22
import {deprecatedLibraries, deprecatedThemeLibraries} from "../../utils/deprecations.js";
33
import {DataWithPosition, fromYaml, getPosition} from "data-with-position";
44
import {MESSAGE} from "../messages.js";
@@ -20,6 +20,8 @@ interface YamlWithPosInfo extends DataWithPosition {
2020
};
2121
};
2222
}
23+
// This regex is derived from the single-line variant defined in ui5types/directives.ts
24+
const DIRECTIVE_REGEX = /#\s*ui5lint-(enable|disable)(?:-((?:next-)?line))?([ \t]+(?:[\w-]+[ \t]*,[ \t]*)*(?:[ \t]*[\w-]+))?[ \t]*,?[ \t]*(?:--.*)?$/mg;
2325

2426
export default class YamlLinter {
2527
#content;
@@ -45,6 +47,8 @@ export default class YamlLinter {
4547
const parsedYamlWithPosInfo: YamlWithPosInfo = this.#parseYaml(document);
4648
// Analyze part content with line number offset
4749
this.#analyzeYaml(parsedYamlWithPosInfo, lineNumberOffset);
50+
this.#collectDirectives(document, lineNumberOffset);
51+
4852
// Update line number offset for next part
4953
lineNumberOffset += document.split(/\r\n|\r|\n/g).length;
5054
});
@@ -79,4 +83,29 @@ export default class YamlLinter {
7983
}
8084
});
8185
}
86+
87+
#collectDirectives(content: string, offset: number) {
88+
const matches = content.matchAll(DIRECTIVE_REGEX);
89+
for (const match of matches) {
90+
const action = (match[1] ?? match[4]) as DirectiveAction;
91+
const scope = (match[2] ?? match[5]) as DirectiveScope;
92+
const rules = match[3] ?? match[6];
93+
94+
let ruleNames = rules?.split(",") ?? [];
95+
ruleNames = ruleNames.map((rule) => rule.trim());
96+
97+
// Determine line and column of match
98+
const left = content.slice(0, match.index);
99+
const line = (left.match(/\n/g) ?? []).length + 1 + offset;
100+
const lastIndexOf = left.lastIndexOf("\n") + 1;
101+
const column = match.index - lastIndexOf + 1;
102+
103+
this.#context.addDirective(this.#resourcePath, {
104+
action,
105+
scope, ruleNames,
106+
line,
107+
column,
108+
});
109+
}
110+
}
82111
}

0 commit comments

Comments
 (0)