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
|
-
|
|
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
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
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. ${
|
|
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
|
-
|
|
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
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
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. ${
|
|
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