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