@herb-tools/linter 0.4.2 → 0.5.0
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/README.md +221 -10
- package/dist/herb-lint.js +817 -292
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +360 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +355 -83
- package/dist/index.js.map +1 -1
- package/dist/package.json +4 -4
- package/dist/src/cli/argument-parser.js +28 -18
- package/dist/src/cli/argument-parser.js.map +1 -1
- package/dist/src/cli/file-processor.js +21 -17
- package/dist/src/cli/file-processor.js.map +1 -1
- package/dist/src/cli/formatters/detailed-formatter.js +9 -9
- package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
- package/dist/src/cli/formatters/github-actions-formatter.js +50 -0
- package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -0
- package/dist/src/cli/formatters/index.js +2 -0
- package/dist/src/cli/formatters/index.js.map +1 -1
- package/dist/src/cli/formatters/json-formatter.js +58 -0
- package/dist/src/cli/formatters/json-formatter.js.map +1 -0
- package/dist/src/cli/formatters/simple-formatter.js +15 -15
- package/dist/src/cli/formatters/simple-formatter.js.map +1 -1
- package/dist/src/cli/output-manager.js +120 -0
- package/dist/src/cli/output-manager.js.map +1 -0
- package/dist/src/cli/summary-reporter.js +22 -22
- package/dist/src/cli/summary-reporter.js.map +1 -1
- package/dist/src/cli.js +41 -26
- package/dist/src/cli.js.map +1 -1
- package/dist/src/default-rules.js +8 -0
- package/dist/src/default-rules.js.map +1 -1
- package/dist/src/linter.js +37 -6
- package/dist/src/linter.js.map +1 -1
- package/dist/src/rules/erb-no-empty-tags.js +5 -4
- package/dist/src/rules/erb-no-empty-tags.js.map +1 -1
- package/dist/src/rules/erb-no-output-control-flow.js +5 -4
- package/dist/src/rules/erb-no-output-control-flow.js.map +1 -1
- package/dist/src/rules/erb-prefer-image-tag-helper.js +93 -0
- package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -0
- package/dist/src/rules/erb-require-whitespace-inside-tags.js +5 -4
- package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
- package/dist/src/rules/erb-requires-trailing-newline.js +22 -0
- package/dist/src/rules/erb-requires-trailing-newline.js.map +1 -0
- package/dist/src/rules/html-anchor-require-href.js +5 -4
- package/dist/src/rules/html-anchor-require-href.js.map +1 -1
- package/dist/src/rules/html-aria-attribute-must-be-valid.js +5 -4
- package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -1
- package/dist/src/rules/html-aria-level-must-be-valid.js +27 -0
- package/dist/src/rules/html-aria-level-must-be-valid.js.map +1 -0
- package/dist/src/rules/html-aria-role-heading-requires-level.js +5 -4
- package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -1
- package/dist/src/rules/html-aria-role-must-be-valid.js +5 -4
- package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
- package/dist/src/rules/html-attribute-double-quotes.js +5 -4
- package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
- package/dist/src/rules/html-attribute-values-require-quotes.js +5 -4
- package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -1
- package/dist/src/rules/html-boolean-attributes-no-value.js +5 -4
- package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
- package/dist/src/rules/html-img-require-alt.js +5 -4
- package/dist/src/rules/html-img-require-alt.js.map +1 -1
- package/dist/src/rules/html-no-block-inside-inline.js +7 -6
- package/dist/src/rules/html-no-block-inside-inline.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-attributes.js +5 -4
- package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-ids.js +5 -4
- package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
- package/dist/src/rules/html-no-empty-headings.js +5 -4
- package/dist/src/rules/html-no-empty-headings.js.map +1 -1
- package/dist/src/rules/html-no-nested-links.js +5 -4
- package/dist/src/rules/html-no-nested-links.js.map +1 -1
- package/dist/src/rules/html-tag-name-lowercase.js +5 -4
- package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
- package/dist/src/rules/index.js +3 -0
- package/dist/src/rules/index.js.map +1 -1
- package/dist/src/rules/parser-no-errors.js +18 -0
- package/dist/src/rules/parser-no-errors.js.map +1 -0
- package/dist/src/rules/rule-utils.js +125 -2
- package/dist/src/rules/rule-utils.js.map +1 -1
- package/dist/src/rules/svg-tag-name-capitalization.js +5 -4
- package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
- package/dist/src/types.js +15 -1
- package/dist/src/types.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/cli/argument-parser.d.ts +2 -1
- package/dist/types/cli/file-processor.d.ts +6 -5
- package/dist/types/cli/formatters/base-formatter.d.ts +2 -2
- package/dist/types/cli/formatters/detailed-formatter.d.ts +2 -2
- package/dist/types/cli/formatters/github-actions-formatter.d.ts +12 -0
- package/dist/types/cli/formatters/index.d.ts +2 -0
- package/dist/types/cli/formatters/json-formatter.d.ts +42 -0
- package/dist/types/cli/formatters/simple-formatter.d.ts +2 -2
- package/dist/types/cli/output-manager.d.ts +31 -0
- package/dist/types/cli/summary-reporter.d.ts +3 -3
- package/dist/types/cli.d.ts +3 -1
- package/dist/types/linter.d.ts +20 -5
- package/dist/types/rules/erb-no-empty-tags.d.ts +5 -4
- package/dist/types/rules/erb-no-output-control-flow.d.ts +5 -4
- package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +7 -0
- package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +5 -4
- package/dist/types/rules/erb-requires-trailing-newline.d.ts +6 -0
- package/dist/types/rules/html-anchor-require-href.d.ts +5 -4
- package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +5 -4
- package/dist/types/rules/html-aria-level-must-be-valid.d.ts +7 -0
- package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +5 -4
- package/dist/types/rules/html-aria-role-must-be-valid.d.ts +5 -4
- package/dist/types/rules/html-attribute-double-quotes.d.ts +5 -4
- package/dist/types/rules/html-attribute-values-require-quotes.d.ts +5 -4
- package/dist/types/rules/html-boolean-attributes-no-value.d.ts +5 -4
- package/dist/types/rules/html-img-require-alt.d.ts +5 -4
- package/dist/types/rules/html-no-block-inside-inline.d.ts +5 -4
- package/dist/types/rules/html-no-duplicate-attributes.d.ts +5 -4
- package/dist/types/rules/html-no-duplicate-ids.d.ts +5 -4
- package/dist/types/rules/html-no-empty-headings.d.ts +5 -4
- package/dist/types/rules/html-no-nested-links.d.ts +5 -4
- package/dist/types/rules/html-tag-name-lowercase.d.ts +5 -4
- package/dist/types/rules/index.d.ts +3 -0
- package/dist/types/rules/parser-no-errors.d.ts +8 -0
- package/dist/types/rules/rule-utils.d.ts +73 -4
- package/dist/types/rules/svg-tag-name-capitalization.d.ts +5 -4
- package/dist/types/src/cli/argument-parser.d.ts +2 -1
- package/dist/types/src/cli/file-processor.d.ts +6 -5
- package/dist/types/src/cli/formatters/base-formatter.d.ts +2 -2
- package/dist/types/src/cli/formatters/detailed-formatter.d.ts +2 -2
- package/dist/types/src/cli/formatters/github-actions-formatter.d.ts +12 -0
- package/dist/types/src/cli/formatters/index.d.ts +2 -0
- package/dist/types/src/cli/formatters/json-formatter.d.ts +42 -0
- package/dist/types/src/cli/formatters/simple-formatter.d.ts +2 -2
- package/dist/types/src/cli/output-manager.d.ts +31 -0
- package/dist/types/src/cli/summary-reporter.d.ts +3 -3
- package/dist/types/src/cli.d.ts +3 -1
- package/dist/types/src/linter.d.ts +20 -5
- package/dist/types/src/rules/erb-no-empty-tags.d.ts +5 -4
- package/dist/types/src/rules/erb-no-output-control-flow.d.ts +5 -4
- package/dist/types/src/rules/erb-prefer-image-tag-helper.d.ts +7 -0
- package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +5 -4
- package/dist/types/src/rules/erb-requires-trailing-newline.d.ts +6 -0
- package/dist/types/src/rules/html-anchor-require-href.d.ts +5 -4
- package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +5 -4
- package/dist/types/src/rules/html-aria-level-must-be-valid.d.ts +7 -0
- package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +5 -4
- package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +5 -4
- package/dist/types/src/rules/html-attribute-double-quotes.d.ts +5 -4
- package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +5 -4
- package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +5 -4
- package/dist/types/src/rules/html-img-require-alt.d.ts +5 -4
- package/dist/types/src/rules/html-no-block-inside-inline.d.ts +5 -4
- package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +5 -4
- package/dist/types/src/rules/html-no-duplicate-ids.d.ts +5 -4
- package/dist/types/src/rules/html-no-empty-headings.d.ts +5 -4
- package/dist/types/src/rules/html-no-nested-links.d.ts +5 -4
- package/dist/types/src/rules/html-tag-name-lowercase.d.ts +5 -4
- package/dist/types/src/rules/index.d.ts +3 -0
- package/dist/types/src/rules/parser-no-errors.d.ts +8 -0
- package/dist/types/src/rules/rule-utils.d.ts +73 -4
- package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +5 -4
- package/dist/types/src/types.d.ts +50 -7
- package/dist/types/types.d.ts +50 -7
- package/docs/rules/README.md +5 -1
- package/docs/rules/erb-prefer-image-tag-helper.md +65 -0
- package/docs/rules/erb-requires-trailing-newline.md +37 -0
- package/docs/rules/html-anchor-require-href.md +1 -1
- package/docs/rules/html-aria-level-must-be-valid.md +37 -0
- package/docs/rules/parser-no-errors.md +84 -0
- package/package.json +4 -4
- package/src/cli/argument-parser.ts +33 -19
- package/src/cli/file-processor.ts +27 -21
- package/src/cli/formatters/base-formatter.ts +2 -2
- package/src/cli/formatters/detailed-formatter.ts +9 -9
- package/src/cli/formatters/github-actions-formatter.ts +70 -0
- package/src/cli/formatters/index.ts +2 -0
- package/src/cli/formatters/json-formatter.ts +107 -0
- package/src/cli/formatters/simple-formatter.ts +15 -15
- package/src/cli/output-manager.ts +143 -0
- package/src/cli/summary-reporter.ts +24 -24
- package/src/cli.ts +48 -31
- package/src/default-rules.ts +8 -0
- package/src/linter.ts +42 -8
- package/src/rules/erb-no-empty-tags.ts +7 -6
- package/src/rules/erb-no-output-control-flow.ts +8 -6
- package/src/rules/erb-prefer-image-tag-helper.ts +124 -0
- package/src/rules/erb-require-whitespace-inside-tags.ts +7 -6
- package/src/rules/erb-requires-trailing-newline.ts +29 -0
- package/src/rules/html-anchor-require-href.ts +7 -6
- package/src/rules/html-aria-attribute-must-be-valid.ts +7 -7
- package/src/rules/html-aria-level-must-be-valid.ts +42 -0
- package/src/rules/html-aria-role-heading-requires-level.ts +7 -6
- package/src/rules/html-aria-role-must-be-valid.ts +7 -6
- package/src/rules/html-attribute-double-quotes.ts +7 -6
- package/src/rules/html-attribute-values-require-quotes.ts +7 -6
- package/src/rules/html-boolean-attributes-no-value.ts +7 -6
- package/src/rules/html-img-require-alt.ts +7 -6
- package/src/rules/html-no-block-inside-inline.ts +9 -8
- package/src/rules/html-no-duplicate-attributes.ts +7 -6
- package/src/rules/html-no-duplicate-ids.ts +7 -7
- package/src/rules/html-no-empty-headings.ts +7 -6
- package/src/rules/html-no-nested-links.ts +7 -6
- package/src/rules/html-tag-name-lowercase.ts +7 -6
- package/src/rules/index.ts +3 -0
- package/src/rules/parser-no-errors.ts +25 -0
- package/src/rules/rule-utils.ts +156 -4
- package/src/rules/svg-tag-name-capitalization.ts +7 -6
- package/src/types.ts +61 -7
package/dist/index.cjs
CHANGED
|
@@ -2,15 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
var core = require('@herb-tools/core');
|
|
4
4
|
|
|
5
|
+
class ParserRule {
|
|
6
|
+
static type = "parser";
|
|
7
|
+
}
|
|
8
|
+
class LexerRule {
|
|
9
|
+
static type = "lexer";
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Default context object with all keys defined but set to undefined
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_LINT_CONTEXT = {
|
|
15
|
+
fileName: undefined
|
|
16
|
+
};
|
|
17
|
+
class SourceRule {
|
|
18
|
+
static type = "source";
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
/**
|
|
6
22
|
* Base visitor class that provides common functionality for rule visitors
|
|
7
23
|
*/
|
|
8
24
|
class BaseRuleVisitor extends core.Visitor {
|
|
9
25
|
offenses = [];
|
|
10
26
|
ruleName;
|
|
11
|
-
|
|
27
|
+
context;
|
|
28
|
+
constructor(ruleName, context) {
|
|
12
29
|
super();
|
|
13
30
|
this.ruleName = ruleName;
|
|
31
|
+
this.context = { ...DEFAULT_LINT_CONTEXT, ...context };
|
|
14
32
|
}
|
|
15
33
|
/**
|
|
16
34
|
* Helper method to create a lint offense
|
|
@@ -253,6 +271,19 @@ const ARIA_ATTRIBUTES = new Set([
|
|
|
253
271
|
'aria-valuenow',
|
|
254
272
|
'aria-valuetext',
|
|
255
273
|
]);
|
|
274
|
+
/**
|
|
275
|
+
* Helper function to create a location at the end of the source with a 1-character range
|
|
276
|
+
*/
|
|
277
|
+
function createEndOfFileLocation(source) {
|
|
278
|
+
const lines = source.split('\n');
|
|
279
|
+
const lastLineNumber = lines.length;
|
|
280
|
+
const lastLine = lines[lines.length - 1];
|
|
281
|
+
const lastColumnNumber = lastLine.length;
|
|
282
|
+
const startColumn = lastColumnNumber > 0 ? lastColumnNumber - 1 : 0;
|
|
283
|
+
const start = new core.Position(lastLineNumber, startColumn);
|
|
284
|
+
const end = new core.Position(lastLineNumber, lastColumnNumber);
|
|
285
|
+
return new core.Location(start, end);
|
|
286
|
+
}
|
|
256
287
|
/**
|
|
257
288
|
* Checks if an element is inline
|
|
258
289
|
*/
|
|
@@ -277,6 +308,9 @@ function isBooleanAttribute(attributeName) {
|
|
|
277
308
|
* and attribute iteration logic. Provides simplified interface with extracted attribute info.
|
|
278
309
|
*/
|
|
279
310
|
class AttributeVisitorMixin extends BaseRuleVisitor {
|
|
311
|
+
constructor(ruleName, context) {
|
|
312
|
+
super(ruleName, context);
|
|
313
|
+
}
|
|
280
314
|
visitHTMLOpenTagNode(node) {
|
|
281
315
|
this.checkAttributesOnNode(node);
|
|
282
316
|
super.visitHTMLOpenTagNode(node);
|
|
@@ -306,6 +340,56 @@ function forEachAttribute(node, callback) {
|
|
|
306
340
|
}
|
|
307
341
|
}
|
|
308
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* Base source visitor class that provides common functionality for source-based rule visitors
|
|
345
|
+
*/
|
|
346
|
+
class BaseSourceRuleVisitor {
|
|
347
|
+
offenses = [];
|
|
348
|
+
ruleName;
|
|
349
|
+
context;
|
|
350
|
+
constructor(ruleName, context) {
|
|
351
|
+
this.ruleName = ruleName;
|
|
352
|
+
this.context = { ...DEFAULT_LINT_CONTEXT, ...context };
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Helper method to create a lint offense for source rules
|
|
356
|
+
*/
|
|
357
|
+
createOffense(message, location, severity = "error") {
|
|
358
|
+
return {
|
|
359
|
+
rule: this.ruleName, // Type assertion for compatibility
|
|
360
|
+
code: this.ruleName,
|
|
361
|
+
source: "Herb Linter",
|
|
362
|
+
message,
|
|
363
|
+
location,
|
|
364
|
+
severity,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Helper method to add an offense to the offenses array
|
|
369
|
+
*/
|
|
370
|
+
addOffense(message, location, severity = "error") {
|
|
371
|
+
this.offenses.push(this.createOffense(message, location, severity));
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Main entry point for source rule visitors
|
|
375
|
+
* @param source - The raw source code
|
|
376
|
+
*/
|
|
377
|
+
visit(source) {
|
|
378
|
+
this.visitSource(source);
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Helper method to create a location for a specific position in the source
|
|
382
|
+
*/
|
|
383
|
+
createLocationAt(source, position) {
|
|
384
|
+
const beforePosition = source.substring(0, position);
|
|
385
|
+
const lines = beforePosition.split('\n');
|
|
386
|
+
const line = lines.length;
|
|
387
|
+
const column = lines[lines.length - 1].length + 1;
|
|
388
|
+
const start = new core.Position(line, column);
|
|
389
|
+
const end = new core.Position(line, column);
|
|
390
|
+
return new core.Location(start, end);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
309
393
|
|
|
310
394
|
class ERBNoEmptyTagsVisitor extends BaseRuleVisitor {
|
|
311
395
|
visitERBContentNode(node) {
|
|
@@ -320,11 +404,11 @@ class ERBNoEmptyTagsVisitor extends BaseRuleVisitor {
|
|
|
320
404
|
this.addOffense("ERB tag should not be empty. Remove empty ERB tags or add content.", node.location, "error");
|
|
321
405
|
}
|
|
322
406
|
}
|
|
323
|
-
class ERBNoEmptyTagsRule {
|
|
407
|
+
class ERBNoEmptyTagsRule extends ParserRule {
|
|
324
408
|
name = "erb-no-empty-tags";
|
|
325
|
-
check(
|
|
326
|
-
const visitor = new ERBNoEmptyTagsVisitor(this.name);
|
|
327
|
-
visitor.visit(
|
|
409
|
+
check(result, context) {
|
|
410
|
+
const visitor = new ERBNoEmptyTagsVisitor(this.name, context);
|
|
411
|
+
visitor.visit(result.value);
|
|
328
412
|
return visitor.offenses;
|
|
329
413
|
}
|
|
330
414
|
}
|
|
@@ -366,11 +450,122 @@ class ERBNoOutputControlFlowRuleVisitor extends BaseRuleVisitor {
|
|
|
366
450
|
return;
|
|
367
451
|
}
|
|
368
452
|
}
|
|
369
|
-
class ERBNoOutputControlFlowRule {
|
|
453
|
+
class ERBNoOutputControlFlowRule extends ParserRule {
|
|
370
454
|
name = "erb-no-output-control-flow";
|
|
371
|
-
check(
|
|
372
|
-
const visitor = new ERBNoOutputControlFlowRuleVisitor(this.name);
|
|
373
|
-
visitor.visit(
|
|
455
|
+
check(result, context) {
|
|
456
|
+
const visitor = new ERBNoOutputControlFlowRuleVisitor(this.name, context);
|
|
457
|
+
visitor.visit(result.value);
|
|
458
|
+
return visitor.offenses;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
class ERBPreferImageTagHelperVisitor extends BaseRuleVisitor {
|
|
463
|
+
visitHTMLOpenTagNode(node) {
|
|
464
|
+
this.checkImgTag(node);
|
|
465
|
+
super.visitHTMLOpenTagNode(node);
|
|
466
|
+
}
|
|
467
|
+
visitHTMLSelfCloseTagNode(node) {
|
|
468
|
+
this.checkImgTag(node);
|
|
469
|
+
super.visitHTMLSelfCloseTagNode(node);
|
|
470
|
+
}
|
|
471
|
+
checkImgTag(node) {
|
|
472
|
+
const tagName = getTagName(node);
|
|
473
|
+
if (tagName !== "img") {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const attributes = getAttributes(node);
|
|
477
|
+
const srcAttribute = findAttributeByName(attributes, "src");
|
|
478
|
+
if (!srcAttribute) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (!srcAttribute.value) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const valueNode = srcAttribute.value;
|
|
485
|
+
const hasERBContent = this.containsERBContent(valueNode);
|
|
486
|
+
if (hasERBContent) {
|
|
487
|
+
const suggestedExpression = this.buildSuggestedExpression(valueNode);
|
|
488
|
+
this.addOffense(`Prefer \`image_tag\` helper over manual \`<img>\` with dynamic ERB expressions. Use \`<%= image_tag ${suggestedExpression}, alt: "..." %>\` instead.`, srcAttribute.location, "warning");
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
containsERBContent(valueNode) {
|
|
492
|
+
if (!valueNode.children)
|
|
493
|
+
return false;
|
|
494
|
+
return valueNode.children.some(child => child.type === "AST_ERB_CONTENT_NODE");
|
|
495
|
+
}
|
|
496
|
+
buildSuggestedExpression(valueNode) {
|
|
497
|
+
if (!valueNode.children)
|
|
498
|
+
return "expression";
|
|
499
|
+
let hasText = false;
|
|
500
|
+
let hasERB = false;
|
|
501
|
+
for (const child of valueNode.children) {
|
|
502
|
+
if (child.type === "AST_ERB_CONTENT_NODE") {
|
|
503
|
+
hasERB = true;
|
|
504
|
+
}
|
|
505
|
+
else if (child.type === "AST_LITERAL_NODE") {
|
|
506
|
+
const literalNode = child;
|
|
507
|
+
if (literalNode.content && literalNode.content.trim()) {
|
|
508
|
+
hasText = true;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (hasText && hasERB) {
|
|
513
|
+
let result = '"';
|
|
514
|
+
for (const child of valueNode.children) {
|
|
515
|
+
if (child.type === "AST_ERB_CONTENT_NODE") {
|
|
516
|
+
const erbNode = child;
|
|
517
|
+
result += `#{${(erbNode.content?.value || "").trim()}}`;
|
|
518
|
+
}
|
|
519
|
+
else if (child.type === "AST_LITERAL_NODE") {
|
|
520
|
+
const literalNode = child;
|
|
521
|
+
result += literalNode.content || "";
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
result += '"';
|
|
525
|
+
return result;
|
|
526
|
+
}
|
|
527
|
+
if (hasERB && !hasText) {
|
|
528
|
+
const erbNodes = valueNode.children.filter(child => child.type === "AST_ERB_CONTENT_NODE");
|
|
529
|
+
if (erbNodes.length === 1) {
|
|
530
|
+
return (erbNodes[0].content?.value || "").trim();
|
|
531
|
+
}
|
|
532
|
+
else if (erbNodes.length > 1) {
|
|
533
|
+
let result = '"';
|
|
534
|
+
for (const erbNode of erbNodes) {
|
|
535
|
+
result += `#{${(erbNode.content?.value || "").trim()}}`;
|
|
536
|
+
}
|
|
537
|
+
result += '"';
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return "expression";
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
class ERBPreferImageTagHelperRule extends ParserRule {
|
|
545
|
+
name = "erb-prefer-image-tag-helper";
|
|
546
|
+
check(result, context) {
|
|
547
|
+
const visitor = new ERBPreferImageTagHelperVisitor(this.name, context);
|
|
548
|
+
visitor.visit(result.value);
|
|
549
|
+
return visitor.offenses;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
class ERBRequiresTrailingNewlineVisitor extends BaseSourceRuleVisitor {
|
|
554
|
+
visitSource(source) {
|
|
555
|
+
if (source.length === 0)
|
|
556
|
+
return;
|
|
557
|
+
if (source.endsWith('\n'))
|
|
558
|
+
return;
|
|
559
|
+
if (!this.context.fileName)
|
|
560
|
+
return;
|
|
561
|
+
this.addOffense("File must end with trailing newline", createEndOfFileLocation(source), "error");
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
class ERBRequiresTrailingNewlineRule extends SourceRule {
|
|
565
|
+
name = "erb-requires-trailing-newline";
|
|
566
|
+
check(source, context) {
|
|
567
|
+
const visitor = new ERBRequiresTrailingNewlineVisitor(this.name, context);
|
|
568
|
+
visitor.visit(source);
|
|
374
569
|
return visitor.offenses;
|
|
375
570
|
}
|
|
376
571
|
}
|
|
@@ -423,11 +618,11 @@ class RequireWhitespaceInsideTags extends BaseRuleVisitor {
|
|
|
423
618
|
this.addOffense(`Add whitespace before \`${closeTag.value}\`.`, closeTag.location, "error");
|
|
424
619
|
}
|
|
425
620
|
}
|
|
426
|
-
class ERBRequireWhitespaceRule {
|
|
621
|
+
class ERBRequireWhitespaceRule extends ParserRule {
|
|
427
622
|
name = "erb-require-whitespace-inside-tags";
|
|
428
|
-
check(
|
|
429
|
-
const visitor = new RequireWhitespaceInsideTags(this.name);
|
|
430
|
-
visitor.visit(
|
|
623
|
+
check(result, context) {
|
|
624
|
+
const visitor = new RequireWhitespaceInsideTags(this.name, context);
|
|
625
|
+
visitor.visit(result.value);
|
|
431
626
|
return visitor.offenses;
|
|
432
627
|
}
|
|
433
628
|
}
|
|
@@ -447,11 +642,11 @@ class AnchorRechireHrefVisitor extends BaseRuleVisitor {
|
|
|
447
642
|
}
|
|
448
643
|
}
|
|
449
644
|
}
|
|
450
|
-
class HTMLAnchorRequireHrefRule {
|
|
645
|
+
class HTMLAnchorRequireHrefRule extends ParserRule {
|
|
451
646
|
name = "html-anchor-require-href";
|
|
452
|
-
check(
|
|
453
|
-
const visitor = new AnchorRechireHrefVisitor(this.name);
|
|
454
|
-
visitor.visit(
|
|
647
|
+
check(result, context) {
|
|
648
|
+
const visitor = new AnchorRechireHrefVisitor(this.name, context);
|
|
649
|
+
visitor.visit(result.value);
|
|
455
650
|
return visitor.offenses;
|
|
456
651
|
}
|
|
457
652
|
}
|
|
@@ -470,11 +665,36 @@ class AriaAttributeMustBeValid extends AttributeVisitorMixin {
|
|
|
470
665
|
}
|
|
471
666
|
}
|
|
472
667
|
}
|
|
473
|
-
class HTMLAriaAttributeMustBeValid {
|
|
668
|
+
class HTMLAriaAttributeMustBeValid extends ParserRule {
|
|
474
669
|
name = "html-aria-attribute-must-be-valid";
|
|
475
|
-
check(
|
|
476
|
-
const visitor = new AriaAttributeMustBeValid(this.name);
|
|
477
|
-
visitor.visit(
|
|
670
|
+
check(result, context) {
|
|
671
|
+
const visitor = new AriaAttributeMustBeValid(this.name, context);
|
|
672
|
+
visitor.visit(result.value);
|
|
673
|
+
return visitor.offenses;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
class HTMLAriaLevelMustBeValidVisitor extends AttributeVisitorMixin {
|
|
678
|
+
checkAttribute(attributeName, attributeValue, attributeNode, _parentNode) {
|
|
679
|
+
if (attributeName !== "aria-level")
|
|
680
|
+
return;
|
|
681
|
+
if (attributeValue !== null && attributeValue.includes("<%"))
|
|
682
|
+
return;
|
|
683
|
+
if (attributeValue === null || attributeValue === "") {
|
|
684
|
+
this.addOffense(`The \`aria-level\` attribute must be an integer between 1 and 6, got an empty value.`, attributeNode.location);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const number = parseInt(attributeValue);
|
|
688
|
+
if (isNaN(number) || number < 1 || number > 6 || attributeValue !== number.toString()) {
|
|
689
|
+
this.addOffense(`The \`aria-level\` attribute must be an integer between 1 and 6, got \`${attributeValue}\`.`, attributeNode.location);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
class HTMLAriaLevelMustBeValidRule extends ParserRule {
|
|
694
|
+
name = "html-aria-level-must-be-valid";
|
|
695
|
+
check(result, context) {
|
|
696
|
+
const visitor = new HTMLAriaLevelMustBeValidVisitor(this.name, context);
|
|
697
|
+
visitor.visit(result.value);
|
|
478
698
|
return visitor.offenses;
|
|
479
699
|
}
|
|
480
700
|
}
|
|
@@ -495,11 +715,11 @@ class AriaRoleHeadingRequiresLevel extends AttributeVisitorMixin {
|
|
|
495
715
|
}
|
|
496
716
|
}
|
|
497
717
|
}
|
|
498
|
-
class HTMLAriaRoleHeadingRequiresLevelRule {
|
|
718
|
+
class HTMLAriaRoleHeadingRequiresLevelRule extends ParserRule {
|
|
499
719
|
name = "html-aria-role-heading-requires-level";
|
|
500
|
-
check(
|
|
501
|
-
const visitor = new AriaRoleHeadingRequiresLevel(this.name);
|
|
502
|
-
visitor.visit(
|
|
720
|
+
check(result, context) {
|
|
721
|
+
const visitor = new AriaRoleHeadingRequiresLevel(this.name, context);
|
|
722
|
+
visitor.visit(result.value);
|
|
503
723
|
return visitor.offenses;
|
|
504
724
|
}
|
|
505
725
|
}
|
|
@@ -515,11 +735,11 @@ class AriaRoleMustBeValid extends AttributeVisitorMixin {
|
|
|
515
735
|
this.addOffense(`The \`role\` attribute must be a valid ARIA role. Role \`${attributeValue}\` is not recognized.`, attributeNode.location, "error");
|
|
516
736
|
}
|
|
517
737
|
}
|
|
518
|
-
class HTMLAriaRoleMustBeValidRule {
|
|
738
|
+
class HTMLAriaRoleMustBeValidRule extends ParserRule {
|
|
519
739
|
name = "html-aria-role-must-be-valid";
|
|
520
|
-
check(
|
|
521
|
-
const visitor = new AriaRoleMustBeValid(this.name);
|
|
522
|
-
visitor.visit(
|
|
740
|
+
check(result, context) {
|
|
741
|
+
const visitor = new AriaRoleMustBeValid(this.name, context);
|
|
742
|
+
visitor.visit(result.value);
|
|
523
743
|
return visitor.offenses;
|
|
524
744
|
}
|
|
525
745
|
}
|
|
@@ -535,11 +755,11 @@ class AttributeDoubleQuotesVisitor extends AttributeVisitorMixin {
|
|
|
535
755
|
this.addOffense(`Attribute \`${attributeName}\` uses single quotes. Prefer double quotes for HTML attribute values: \`${attributeName}="value"\`.`, attributeNode.value.location, "warning");
|
|
536
756
|
}
|
|
537
757
|
}
|
|
538
|
-
class HTMLAttributeDoubleQuotesRule {
|
|
758
|
+
class HTMLAttributeDoubleQuotesRule extends ParserRule {
|
|
539
759
|
name = "html-attribute-double-quotes";
|
|
540
|
-
check(
|
|
541
|
-
const visitor = new AttributeDoubleQuotesVisitor(this.name);
|
|
542
|
-
visitor.visit(
|
|
760
|
+
check(result, context) {
|
|
761
|
+
const visitor = new AttributeDoubleQuotesVisitor(this.name, context);
|
|
762
|
+
visitor.visit(result.value);
|
|
543
763
|
return visitor.offenses;
|
|
544
764
|
}
|
|
545
765
|
}
|
|
@@ -556,11 +776,11 @@ class AttributeValuesRequireQuotesVisitor extends AttributeVisitorMixin {
|
|
|
556
776
|
`Attribute value should be quoted: \`${attributeName}="value"\`. Always wrap attribute values in quotes.`, valueNode.location, "error");
|
|
557
777
|
}
|
|
558
778
|
}
|
|
559
|
-
class HTMLAttributeValuesRequireQuotesRule {
|
|
779
|
+
class HTMLAttributeValuesRequireQuotesRule extends ParserRule {
|
|
560
780
|
name = "html-attribute-values-require-quotes";
|
|
561
|
-
check(
|
|
562
|
-
const visitor = new AttributeValuesRequireQuotesVisitor(this.name);
|
|
563
|
-
visitor.visit(
|
|
781
|
+
check(result, context) {
|
|
782
|
+
const visitor = new AttributeValuesRequireQuotesVisitor(this.name, context);
|
|
783
|
+
visitor.visit(result.value);
|
|
564
784
|
return visitor.offenses;
|
|
565
785
|
}
|
|
566
786
|
}
|
|
@@ -574,11 +794,11 @@ class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
|
|
|
574
794
|
this.addOffense(`Boolean attribute \`${attributeName}\` should not have a value. Use \`${attributeName}\` instead of \`${attributeName}="${attributeName}"\`.`, attributeNode.value.location, "error");
|
|
575
795
|
}
|
|
576
796
|
}
|
|
577
|
-
class HTMLBooleanAttributesNoValueRule {
|
|
797
|
+
class HTMLBooleanAttributesNoValueRule extends ParserRule {
|
|
578
798
|
name = "html-boolean-attributes-no-value";
|
|
579
|
-
check(
|
|
580
|
-
const visitor = new BooleanAttributesNoValueVisitor(this.name);
|
|
581
|
-
visitor.visit(
|
|
799
|
+
check(result, context) {
|
|
800
|
+
const visitor = new BooleanAttributesNoValueVisitor(this.name, context);
|
|
801
|
+
visitor.visit(result.value);
|
|
582
802
|
return visitor.offenses;
|
|
583
803
|
}
|
|
584
804
|
}
|
|
@@ -602,11 +822,11 @@ class ImgRequireAltVisitor extends BaseRuleVisitor {
|
|
|
602
822
|
}
|
|
603
823
|
}
|
|
604
824
|
}
|
|
605
|
-
class HTMLImgRequireAltRule {
|
|
825
|
+
class HTMLImgRequireAltRule extends ParserRule {
|
|
606
826
|
name = "html-img-require-alt";
|
|
607
|
-
check(
|
|
608
|
-
const visitor = new ImgRequireAltVisitor(this.name);
|
|
609
|
-
visitor.visit(
|
|
827
|
+
check(result, context) {
|
|
828
|
+
const visitor = new ImgRequireAltVisitor(this.name, context);
|
|
829
|
+
visitor.visit(result.value);
|
|
610
830
|
return visitor.offenses;
|
|
611
831
|
}
|
|
612
832
|
}
|
|
@@ -644,11 +864,11 @@ class NoDuplicateAttributesVisitor extends BaseRuleVisitor {
|
|
|
644
864
|
}
|
|
645
865
|
}
|
|
646
866
|
}
|
|
647
|
-
class HTMLNoDuplicateAttributesRule {
|
|
867
|
+
class HTMLNoDuplicateAttributesRule extends ParserRule {
|
|
648
868
|
name = "html-no-duplicate-attributes";
|
|
649
|
-
check(
|
|
650
|
-
const visitor = new NoDuplicateAttributesVisitor(this.name);
|
|
651
|
-
visitor.visit(
|
|
869
|
+
check(result, context) {
|
|
870
|
+
const visitor = new NoDuplicateAttributesVisitor(this.name, context);
|
|
871
|
+
visitor.visit(result.value);
|
|
652
872
|
return visitor.offenses;
|
|
653
873
|
}
|
|
654
874
|
}
|
|
@@ -668,11 +888,11 @@ class NoDuplicateIdsVisitor extends AttributeVisitorMixin {
|
|
|
668
888
|
this.documentIds.add(id);
|
|
669
889
|
}
|
|
670
890
|
}
|
|
671
|
-
class HTMLNoDuplicateIdsRule {
|
|
891
|
+
class HTMLNoDuplicateIdsRule extends ParserRule {
|
|
672
892
|
name = "html-no-duplicate-ids";
|
|
673
|
-
check(
|
|
674
|
-
const visitor = new NoDuplicateIdsVisitor(this.name);
|
|
675
|
-
visitor.visit(
|
|
893
|
+
check(result, context) {
|
|
894
|
+
const visitor = new NoDuplicateIdsVisitor(this.name, context);
|
|
895
|
+
visitor.visit(result.value);
|
|
676
896
|
return visitor.offenses;
|
|
677
897
|
}
|
|
678
898
|
}
|
|
@@ -815,11 +1035,11 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
|
815
1035
|
return false;
|
|
816
1036
|
}
|
|
817
1037
|
}
|
|
818
|
-
class HTMLNoEmptyHeadingsRule {
|
|
1038
|
+
class HTMLNoEmptyHeadingsRule extends ParserRule {
|
|
819
1039
|
name = "html-no-empty-headings";
|
|
820
|
-
check(
|
|
821
|
-
const visitor = new NoEmptyHeadingsVisitor(this.name);
|
|
822
|
-
visitor.visit(
|
|
1040
|
+
check(result, context) {
|
|
1041
|
+
const visitor = new NoEmptyHeadingsVisitor(this.name, context);
|
|
1042
|
+
visitor.visit(result.value);
|
|
823
1043
|
return visitor.offenses;
|
|
824
1044
|
}
|
|
825
1045
|
}
|
|
@@ -859,11 +1079,11 @@ class NestedLinkVisitor extends BaseRuleVisitor {
|
|
|
859
1079
|
super.visitHTMLOpenTagNode(node);
|
|
860
1080
|
}
|
|
861
1081
|
}
|
|
862
|
-
class HTMLNoNestedLinksRule {
|
|
1082
|
+
class HTMLNoNestedLinksRule extends ParserRule {
|
|
863
1083
|
name = "html-no-nested-links";
|
|
864
|
-
check(
|
|
865
|
-
const visitor = new NestedLinkVisitor(this.name);
|
|
866
|
-
visitor.visit(
|
|
1084
|
+
check(result, context) {
|
|
1085
|
+
const visitor = new NestedLinkVisitor(this.name, context);
|
|
1086
|
+
visitor.visit(result.value);
|
|
867
1087
|
return visitor.offenses;
|
|
868
1088
|
}
|
|
869
1089
|
}
|
|
@@ -906,15 +1126,32 @@ class TagNameLowercaseVisitor extends BaseRuleVisitor {
|
|
|
906
1126
|
}
|
|
907
1127
|
}
|
|
908
1128
|
}
|
|
909
|
-
class HTMLTagNameLowercaseRule {
|
|
1129
|
+
class HTMLTagNameLowercaseRule extends ParserRule {
|
|
910
1130
|
name = "html-tag-name-lowercase";
|
|
911
|
-
check(
|
|
912
|
-
const visitor = new TagNameLowercaseVisitor(this.name);
|
|
913
|
-
visitor.visit(
|
|
1131
|
+
check(result, context) {
|
|
1132
|
+
const visitor = new TagNameLowercaseVisitor(this.name, context);
|
|
1133
|
+
visitor.visit(result.value);
|
|
914
1134
|
return visitor.offenses;
|
|
915
1135
|
}
|
|
916
1136
|
}
|
|
917
1137
|
|
|
1138
|
+
class ParserNoErrorsRule extends ParserRule {
|
|
1139
|
+
name = "parser-no-errors";
|
|
1140
|
+
check(result) {
|
|
1141
|
+
return result.recursiveErrors().map(error => this.herbErrorToLintOffense(error));
|
|
1142
|
+
}
|
|
1143
|
+
herbErrorToLintOffense(error) {
|
|
1144
|
+
return {
|
|
1145
|
+
message: `${error.message} (\`${error.type}\`)`,
|
|
1146
|
+
location: error.location,
|
|
1147
|
+
severity: error.severity,
|
|
1148
|
+
rule: this.name,
|
|
1149
|
+
code: this.name,
|
|
1150
|
+
source: "linter"
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
918
1155
|
class SVGTagNameCapitalizationVisitor extends BaseRuleVisitor {
|
|
919
1156
|
insideSVG = false;
|
|
920
1157
|
visitHTMLElementNode(node) {
|
|
@@ -962,11 +1199,11 @@ class SVGTagNameCapitalizationVisitor extends BaseRuleVisitor {
|
|
|
962
1199
|
}
|
|
963
1200
|
}
|
|
964
1201
|
}
|
|
965
|
-
class SVGTagNameCapitalizationRule {
|
|
1202
|
+
class SVGTagNameCapitalizationRule extends ParserRule {
|
|
966
1203
|
name = "svg-tag-name-capitalization";
|
|
967
|
-
check(
|
|
968
|
-
const visitor = new SVGTagNameCapitalizationVisitor(this.name);
|
|
969
|
-
visitor.visit(
|
|
1204
|
+
check(result, context) {
|
|
1205
|
+
const visitor = new SVGTagNameCapitalizationVisitor(this.name, context);
|
|
1206
|
+
visitor.visit(result.value);
|
|
970
1207
|
return visitor.offenses;
|
|
971
1208
|
}
|
|
972
1209
|
}
|
|
@@ -974,9 +1211,12 @@ class SVGTagNameCapitalizationRule {
|
|
|
974
1211
|
const defaultRules = [
|
|
975
1212
|
ERBNoEmptyTagsRule,
|
|
976
1213
|
ERBNoOutputControlFlowRule,
|
|
1214
|
+
ERBPreferImageTagHelperRule,
|
|
1215
|
+
ERBRequiresTrailingNewlineRule,
|
|
977
1216
|
ERBRequireWhitespaceRule,
|
|
978
1217
|
HTMLAnchorRequireHrefRule,
|
|
979
1218
|
HTMLAriaAttributeMustBeValid,
|
|
1219
|
+
HTMLAriaLevelMustBeValidRule,
|
|
980
1220
|
HTMLAriaRoleHeadingRequiresLevelRule,
|
|
981
1221
|
HTMLAriaRoleMustBeValidRule,
|
|
982
1222
|
HTMLAttributeDoubleQuotesRule,
|
|
@@ -989,17 +1229,21 @@ const defaultRules = [
|
|
|
989
1229
|
HTMLNoEmptyHeadingsRule,
|
|
990
1230
|
HTMLNoNestedLinksRule,
|
|
991
1231
|
HTMLTagNameLowercaseRule,
|
|
1232
|
+
ParserNoErrorsRule,
|
|
992
1233
|
SVGTagNameCapitalizationRule,
|
|
993
1234
|
];
|
|
994
1235
|
|
|
995
1236
|
class Linter {
|
|
996
1237
|
rules;
|
|
1238
|
+
herb;
|
|
997
1239
|
offenses;
|
|
998
1240
|
/**
|
|
999
1241
|
* Creates a new Linter instance.
|
|
1000
|
-
* @param
|
|
1242
|
+
* @param herb - The Herb backend instance for parsing and lexing
|
|
1243
|
+
* @param rules - Array of rule classes (Parser/AST or Lexer) to use. If not provided, uses default rules.
|
|
1001
1244
|
*/
|
|
1002
|
-
constructor(rules) {
|
|
1245
|
+
constructor(herb, rules) {
|
|
1246
|
+
this.herb = herb;
|
|
1003
1247
|
this.rules = rules !== undefined ? rules : this.getDefaultRules();
|
|
1004
1248
|
this.offenses = [];
|
|
1005
1249
|
}
|
|
@@ -1013,11 +1257,39 @@ class Linter {
|
|
|
1013
1257
|
getRuleCount() {
|
|
1014
1258
|
return this.rules.length;
|
|
1015
1259
|
}
|
|
1016
|
-
|
|
1260
|
+
/**
|
|
1261
|
+
* Type guard to check if a rule is a LexerRule
|
|
1262
|
+
*/
|
|
1263
|
+
isLexerRule(rule) {
|
|
1264
|
+
return rule.constructor.type === "lexer";
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Type guard to check if a rule is a SourceRule
|
|
1268
|
+
*/
|
|
1269
|
+
isSourceRule(rule) {
|
|
1270
|
+
return rule.constructor.type === "source";
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Lint source code using Parser/AST, Lexer, and Source rules.
|
|
1274
|
+
* @param source - The source code to lint
|
|
1275
|
+
* @param context - Optional context for linting (e.g., fileName for distinguishing files vs snippets)
|
|
1276
|
+
*/
|
|
1277
|
+
lint(source, context) {
|
|
1017
1278
|
this.offenses = [];
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1279
|
+
const parseResult = this.herb.parse(source);
|
|
1280
|
+
const lexResult = this.herb.lex(source);
|
|
1281
|
+
for (const RuleClass of this.rules) {
|
|
1282
|
+
const rule = new RuleClass();
|
|
1283
|
+
let ruleOffenses;
|
|
1284
|
+
if (this.isLexerRule(rule)) {
|
|
1285
|
+
ruleOffenses = rule.check(lexResult, context);
|
|
1286
|
+
}
|
|
1287
|
+
else if (this.isSourceRule(rule)) {
|
|
1288
|
+
ruleOffenses = rule.check(source, context);
|
|
1289
|
+
}
|
|
1290
|
+
else {
|
|
1291
|
+
ruleOffenses = rule.check(parseResult, context);
|
|
1292
|
+
}
|
|
1021
1293
|
this.offenses.push(...ruleOffenses);
|
|
1022
1294
|
}
|
|
1023
1295
|
const errors = this.offenses.filter(offense => offense.severity === "error").length;
|
|
@@ -1041,7 +1313,7 @@ class BlockInsideInlineVisitor extends BaseRuleVisitor {
|
|
|
1041
1313
|
const isUnknown = !isInline && !isBlock;
|
|
1042
1314
|
return { isInline, isBlock, isUnknown };
|
|
1043
1315
|
}
|
|
1044
|
-
|
|
1316
|
+
addOffenseMessage(tagName, isBlock, openTag) {
|
|
1045
1317
|
const parentInline = this.inlineStack[this.inlineStack.length - 1];
|
|
1046
1318
|
const elementType = isBlock ? "Block-level" : "Unknown";
|
|
1047
1319
|
this.addOffense(`${elementType} element \`<${tagName}>\` cannot be placed inside inline element \`<${parentInline}>\`.`, openTag.tag_name.location, "error");
|
|
@@ -1070,7 +1342,7 @@ class BlockInsideInlineVisitor extends BaseRuleVisitor {
|
|
|
1070
1342
|
}
|
|
1071
1343
|
const { isInline, isBlock, isUnknown } = this.getElementType(tagName);
|
|
1072
1344
|
if ((isBlock || isUnknown) && this.inlineStack.length > 0) {
|
|
1073
|
-
this.
|
|
1345
|
+
this.addOffenseMessage(tagName, isBlock, openTag);
|
|
1074
1346
|
}
|
|
1075
1347
|
if (isInline) {
|
|
1076
1348
|
this.visitInlineElement(node, tagName);
|
|
@@ -1079,18 +1351,22 @@ class BlockInsideInlineVisitor extends BaseRuleVisitor {
|
|
|
1079
1351
|
this.visitBlockElement(node);
|
|
1080
1352
|
}
|
|
1081
1353
|
}
|
|
1082
|
-
class HTMLNoBlockInsideInlineRule {
|
|
1354
|
+
class HTMLNoBlockInsideInlineRule extends ParserRule {
|
|
1083
1355
|
name = "html-no-block-inside-inline";
|
|
1084
|
-
check(
|
|
1085
|
-
const visitor = new BlockInsideInlineVisitor(this.name);
|
|
1086
|
-
visitor.visit(
|
|
1356
|
+
check(result, context) {
|
|
1357
|
+
const visitor = new BlockInsideInlineVisitor(this.name, context);
|
|
1358
|
+
visitor.visit(result.value);
|
|
1087
1359
|
return visitor.offenses;
|
|
1088
1360
|
}
|
|
1089
1361
|
}
|
|
1090
1362
|
|
|
1363
|
+
exports.DEFAULT_LINT_CONTEXT = DEFAULT_LINT_CONTEXT;
|
|
1091
1364
|
exports.ERBNoEmptyTagsRule = ERBNoEmptyTagsRule;
|
|
1092
1365
|
exports.ERBNoOutputControlFlowRule = ERBNoOutputControlFlowRule;
|
|
1366
|
+
exports.ERBPreferImageTagHelperRule = ERBPreferImageTagHelperRule;
|
|
1367
|
+
exports.ERBRequiresTrailingNewlineRule = ERBRequiresTrailingNewlineRule;
|
|
1093
1368
|
exports.HTMLAnchorRequireHrefRule = HTMLAnchorRequireHrefRule;
|
|
1369
|
+
exports.HTMLAriaLevelMustBeValidRule = HTMLAriaLevelMustBeValidRule;
|
|
1094
1370
|
exports.HTMLAriaRoleHeadingRequiresLevelRule = HTMLAriaRoleHeadingRequiresLevelRule;
|
|
1095
1371
|
exports.HTMLAriaRoleMustBeValidRule = HTMLAriaRoleMustBeValidRule;
|
|
1096
1372
|
exports.HTMLAttributeDoubleQuotesRule = HTMLAttributeDoubleQuotesRule;
|
|
@@ -1103,6 +1379,9 @@ exports.HTMLNoDuplicateIdsRule = HTMLNoDuplicateIdsRule;
|
|
|
1103
1379
|
exports.HTMLNoEmptyHeadingsRule = HTMLNoEmptyHeadingsRule;
|
|
1104
1380
|
exports.HTMLNoNestedLinksRule = HTMLNoNestedLinksRule;
|
|
1105
1381
|
exports.HTMLTagNameLowercaseRule = HTMLTagNameLowercaseRule;
|
|
1382
|
+
exports.LexerRule = LexerRule;
|
|
1106
1383
|
exports.Linter = Linter;
|
|
1384
|
+
exports.ParserRule = ParserRule;
|
|
1107
1385
|
exports.SVGTagNameCapitalizationRule = SVGTagNameCapitalizationRule;
|
|
1386
|
+
exports.SourceRule = SourceRule;
|
|
1108
1387
|
//# sourceMappingURL=index.cjs.map
|