zemdomu 1.3.8 → 1.3.10

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.
@@ -318,7 +318,7 @@ class ComponentAnalyzer {
318
318
  return results;
319
319
  }
320
320
  findCrossComponentH1Issues(results) {
321
- var _a;
321
+ var _a, _b;
322
322
  const entryPoints = this.findEntryPoints();
323
323
  const emitted = new Set();
324
324
  const getDisplayName = (component) => {
@@ -326,35 +326,92 @@ class ComponentAnalyzer {
326
326
  return component.name;
327
327
  return path.basename(component.filePath, path.extname(component.filePath));
328
328
  };
329
+ const addResult = (result) => {
330
+ const key = `${result.rule}|${result.filePath}|${result.line}|${result.column}|${result.message}`;
331
+ if (emitted.has(key))
332
+ return;
333
+ emitted.add(key);
334
+ results.push(result);
335
+ };
329
336
  for (const entry of entryPoints) {
330
337
  const comps = this.findComponentsWithRule(entry, 'singleH1', 0);
331
338
  if (comps.length <= 1)
332
339
  continue;
340
+ const conflictMap = new Map();
333
341
  for (const comp of comps) {
334
- if (!comp)
342
+ const conflicts = comps
343
+ .filter(other => other.filePath !== comp.filePath)
344
+ .map(getDisplayName);
345
+ if (conflicts.length)
346
+ conflictMap.set(comp.filePath, conflicts);
347
+ }
348
+ const usageMap = new Map();
349
+ const usageStack = new Set();
350
+ const collectUsage = (component, depth = 0) => {
351
+ if (this.maxDepth !== undefined && depth > this.maxDepth)
352
+ return;
353
+ if (usageStack.has(component.filePath))
354
+ return;
355
+ usageStack.add(component.filePath);
356
+ for (const ref of component.usesComponents) {
357
+ if (!ref.path || !this.componentRegistry.has(ref.path))
358
+ continue;
359
+ const child = this.componentRegistry.get(ref.path);
360
+ if (!usageMap.has(child.filePath))
361
+ usageMap.set(child.filePath, []);
362
+ const locations = ref.usageLocations.length > 0 ? ref.usageLocations : [ref.sourceLocation];
363
+ for (const loc of locations) {
364
+ usageMap.get(child.filePath).push({
365
+ parent: component,
366
+ location: { filePath: component.filePath, line: loc.line, column: loc.column },
367
+ });
368
+ }
369
+ collectUsage(child, depth + 1);
370
+ }
371
+ usageStack.delete(component.filePath);
372
+ };
373
+ collectUsage(entry, 0);
374
+ for (const comp of comps) {
375
+ const conflicts = conflictMap.get(comp.filePath);
376
+ if (!conflicts || !conflicts.length)
335
377
  continue;
336
378
  const compName = getDisplayName(comp);
337
379
  const issues = (_a = comp.issues.get('singleH1')) !== null && _a !== void 0 ? _a : [];
338
380
  if (!issues.length)
339
381
  continue;
340
- const conflictingNames = comps
341
- .filter(other => other.filePath !== comp.filePath)
342
- .map(getDisplayName);
343
- if (!conflictingNames.length)
344
- continue;
345
- const conflicts = conflictingNames.map(name => `'${name}'`).join(', ');
346
- const context = `This <h1> in '${compName}' conflicts with ${conflicts}.`;
382
+ const conflictText = conflicts.map(name => `'${name}'`).join(', ');
383
+ const usageEntries = (_b = usageMap.get(comp.filePath)) !== null && _b !== void 0 ? _b : [];
384
+ const usageRelated = usageEntries.map(u => ({
385
+ filePath: u.location.filePath,
386
+ line: u.location.line,
387
+ column: u.location.column,
388
+ message: `Rendered via '${getDisplayName(u.parent)}'`,
389
+ }));
347
390
  for (const issue of issues) {
348
- const key = `${entry.filePath}|${comp.filePath}|${issue.line}|${issue.column}`;
349
- if (emitted.has(key))
350
- continue;
351
- emitted.add(key);
352
- results.push({
391
+ addResult({
353
392
  filePath: comp.filePath,
354
393
  line: issue.line,
355
394
  column: issue.column,
356
- message: `Multiple <h1> tags across components. ${context}`,
357
- rule: 'singleH1'
395
+ message: `Multiple <h1> tags across components. This <h1> in '${compName}' conflicts with ${conflictText}.`,
396
+ rule: 'singleH1',
397
+ related: usageRelated,
398
+ });
399
+ }
400
+ const childIssueLocations = issues.map(issue => ({
401
+ filePath: comp.filePath,
402
+ line: issue.line,
403
+ column: issue.column,
404
+ message: `Defined in '${compName}'`,
405
+ }));
406
+ for (const usage of usageEntries) {
407
+ const parentName = getDisplayName(usage.parent);
408
+ addResult({
409
+ filePath: usage.location.filePath,
410
+ line: usage.location.line,
411
+ column: usage.location.column,
412
+ message: `Component '${compName}' renders an extra <h1> that conflicts with ${conflictText}.`,
413
+ rule: 'singleH1',
414
+ related: childIssueLocations.length ? childIssueLocations : undefined,
358
415
  });
359
416
  }
360
417
  }
package/out/linter.d.ts CHANGED
@@ -18,6 +18,12 @@ export interface LintResult {
18
18
  rule: string;
19
19
  severity?: RuleSeverity;
20
20
  filePath?: string;
21
+ related?: Array<{
22
+ filePath: string;
23
+ line: number;
24
+ column: number;
25
+ message?: string;
26
+ }>;
21
27
  }
22
28
  export interface Rule {
23
29
  name: string;
@@ -318,7 +318,7 @@ class ComponentAnalyzer {
318
318
  return results;
319
319
  }
320
320
  findCrossComponentH1Issues(results) {
321
- var _a;
321
+ var _a, _b;
322
322
  const entryPoints = this.findEntryPoints();
323
323
  const emitted = new Set();
324
324
  const getDisplayName = (component) => {
@@ -326,35 +326,92 @@ class ComponentAnalyzer {
326
326
  return component.name;
327
327
  return path.basename(component.filePath, path.extname(component.filePath));
328
328
  };
329
+ const addResult = (result) => {
330
+ const key = `${result.rule}|${result.filePath}|${result.line}|${result.column}|${result.message}`;
331
+ if (emitted.has(key))
332
+ return;
333
+ emitted.add(key);
334
+ results.push(result);
335
+ };
329
336
  for (const entry of entryPoints) {
330
337
  const comps = this.findComponentsWithRule(entry, 'singleH1', 0);
331
338
  if (comps.length <= 1)
332
339
  continue;
340
+ const conflictMap = new Map();
333
341
  for (const comp of comps) {
334
- if (!comp)
342
+ const conflicts = comps
343
+ .filter(other => other.filePath !== comp.filePath)
344
+ .map(getDisplayName);
345
+ if (conflicts.length)
346
+ conflictMap.set(comp.filePath, conflicts);
347
+ }
348
+ const usageMap = new Map();
349
+ const usageStack = new Set();
350
+ const collectUsage = (component, depth = 0) => {
351
+ if (this.maxDepth !== undefined && depth > this.maxDepth)
352
+ return;
353
+ if (usageStack.has(component.filePath))
354
+ return;
355
+ usageStack.add(component.filePath);
356
+ for (const ref of component.usesComponents) {
357
+ if (!ref.path || !this.componentRegistry.has(ref.path))
358
+ continue;
359
+ const child = this.componentRegistry.get(ref.path);
360
+ if (!usageMap.has(child.filePath))
361
+ usageMap.set(child.filePath, []);
362
+ const locations = ref.usageLocations.length > 0 ? ref.usageLocations : [ref.sourceLocation];
363
+ for (const loc of locations) {
364
+ usageMap.get(child.filePath).push({
365
+ parent: component,
366
+ location: { filePath: component.filePath, line: loc.line, column: loc.column },
367
+ });
368
+ }
369
+ collectUsage(child, depth + 1);
370
+ }
371
+ usageStack.delete(component.filePath);
372
+ };
373
+ collectUsage(entry, 0);
374
+ for (const comp of comps) {
375
+ const conflicts = conflictMap.get(comp.filePath);
376
+ if (!conflicts || !conflicts.length)
335
377
  continue;
336
378
  const compName = getDisplayName(comp);
337
379
  const issues = (_a = comp.issues.get('singleH1')) !== null && _a !== void 0 ? _a : [];
338
380
  if (!issues.length)
339
381
  continue;
340
- const conflictingNames = comps
341
- .filter(other => other.filePath !== comp.filePath)
342
- .map(getDisplayName);
343
- if (!conflictingNames.length)
344
- continue;
345
- const conflicts = conflictingNames.map(name => `'${name}'`).join(', ');
346
- const context = `This <h1> in '${compName}' conflicts with ${conflicts}.`;
382
+ const conflictText = conflicts.map(name => `'${name}'`).join(', ');
383
+ const usageEntries = (_b = usageMap.get(comp.filePath)) !== null && _b !== void 0 ? _b : [];
384
+ const usageRelated = usageEntries.map(u => ({
385
+ filePath: u.location.filePath,
386
+ line: u.location.line,
387
+ column: u.location.column,
388
+ message: `Rendered via '${getDisplayName(u.parent)}'`,
389
+ }));
347
390
  for (const issue of issues) {
348
- const key = `${entry.filePath}|${comp.filePath}|${issue.line}|${issue.column}`;
349
- if (emitted.has(key))
350
- continue;
351
- emitted.add(key);
352
- results.push({
391
+ addResult({
353
392
  filePath: comp.filePath,
354
393
  line: issue.line,
355
394
  column: issue.column,
356
- message: `Multiple <h1> tags across components. ${context}`,
357
- rule: 'singleH1'
395
+ message: `Multiple <h1> tags across components. This <h1> in '${compName}' conflicts with ${conflictText}.`,
396
+ rule: 'singleH1',
397
+ related: usageRelated,
398
+ });
399
+ }
400
+ const childIssueLocations = issues.map(issue => ({
401
+ filePath: comp.filePath,
402
+ line: issue.line,
403
+ column: issue.column,
404
+ message: `Defined in '${compName}'`,
405
+ }));
406
+ for (const usage of usageEntries) {
407
+ const parentName = getDisplayName(usage.parent);
408
+ addResult({
409
+ filePath: usage.location.filePath,
410
+ line: usage.location.line,
411
+ column: usage.location.column,
412
+ message: `Component '${compName}' renders an extra <h1> that conflicts with ${conflictText}.`,
413
+ rule: 'singleH1',
414
+ related: childIssueLocations.length ? childIssueLocations : undefined,
358
415
  });
359
416
  }
360
417
  }
@@ -46,6 +46,7 @@ describe("cross component heading order", () => {
46
46
  .map((r) => path_1.default.basename(r.filePath)));
47
47
  assert_1.default.ok(singleH1Files.has("Page.tsx"), "Expected cross-component singleH1 to surface on Page.tsx");
48
48
  assert_1.default.ok(singleH1Files.has("Button.tsx"), "Expected cross-component singleH1 to surface on Button.tsx");
49
+ assert_1.default.ok(singleH1Files.has("SubSection.tsx"), "Expected cross-component singleH1 to surface on SubSection.tsx usage");
49
50
  const headingLocations = results
50
51
  .filter((r) => r.rule === "enforceHeadingOrder" && r.filePath)
51
52
  .map((r) => path_1.default.basename(r.filePath));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zemdomu",
3
- "version": "1.3.8",
3
+ "version": "1.3.10",
4
4
  "description": "Semantic HTML linter for HTML, JSX, and TSX. Detects accessibility, SEO, and structure issues before deployment.",
5
5
  "main": "./out/index.js",
6
6
  "types": "./out/index.d.ts",