basalt-ui 1.23.0 → 1.23.1

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.
@@ -400,9 +400,55 @@ const RAW_CHART_LEGEND_ARRAY = /<ChartLegend\b[^>]*?\bitems\s*=\s*\{\s*\[/g
400
400
  // alternative so their `>` never terminates the tag early. A bare `>` comparison inside a prop
401
401
  // expression still ends the match (accepted limitation of the bounded scan).
402
402
  const CHART_ENTRY_POINT_TAG =
403
- /<(?:MultiLine|Bars|BandStrip|Donut|DualPanel|Heatmap|MirroredBars|ZonedLine|StackedArea|LineSparkline|BarSparkline)\b(?:<[^<>]*>)?(?:=>|[^>])*?>/g
403
+ /<(MultiLine|Bars|BandStrip|Donut|DualPanel|Heatmap|MirroredBars|ZonedLine|StackedArea|LineSparkline|BarSparkline)\b(?:<[^<>]*>)?(?:=>|[^>])*?>/g
404
404
  const HAS_ARIA_LABEL_PROP = /\bariaLabel\s*=/
405
405
 
406
+ // ── Tag provenance, shared by the two chart tag rules ────────────────────────────────────────────
407
+ //
408
+ // Both `chart-missing-aria-label` and `unframed-chart` key on a JSX tag NAME, which is the whole of
409
+ // their signal — so a consumer's OWN component that merely shares a shipped kind's name collected
410
+ // the finding too. Reported by a consumer on 1.23.0: a hand-composed local `<MirroredBars>` was
411
+ // told to pass an `ariaLabel` prop it does not accept, by a rule that presents as a correctness
412
+ // finding rather than as a naming one. `shadow-basalt-export` / `ai-sdk-major` scope through
413
+ // `isBasaltScopedFile` (package-level); the analogous file-level signal here is where the name in
414
+ // THIS file came from.
415
+ //
416
+ // The gate is deliberately a one-directional NARROWING: a tag is skipped only when the file DEFINES
417
+ // a component of that name and does not also import it from basalt-ui. Everything else still fires
418
+ // — a tag imported from basalt-ui, one imported from a consumer barrel that re-exports it (`import
419
+ // { MultiLine } from '../charts'`, the shape a downstream component library uses), and one the scan
420
+ // cannot attribute at all. Requiring a POSITIVE basalt import instead would have silently switched
421
+ // both rules off for every barrel-wrapping consumer and for every file with no import statements —
422
+ // a far bigger hole than the false positive it closes.
423
+ const LOCAL_COMPONENT_DEF =
424
+ /(?:^|[\n;}])\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class|const|let|var)\s+([A-Z]\w*)/g
425
+ // `from 'basalt-ui'` and every subpath (`basalt-ui/charts`, `basalt-ui/tokens`, …).
426
+ const BASALT_NAMED_IMPORT =
427
+ /import\s+(?:type\s+)?\{([^}]*)\}\s+from\s+['"]basalt-ui(?:\/[^'"]*)?['"]/g
428
+
429
+ /** Component-shaped names DEFINED in this file — the provable "not the shipped kind" signal. */
430
+ function localComponentNames(codeText: string): ReadonlySet<string> {
431
+ const out = new Set<string>()
432
+ for (const m of codeText.matchAll(LOCAL_COMPONENT_DEF)) out.add(m[1] as string)
433
+ return out
434
+ }
435
+
436
+ /** Names bound in this file by a named import from `basalt-ui` — the local binding, so
437
+ * `{ Bars as BasaltBars }` records `BasaltBars`, which is the name the JSX tag will use. */
438
+ function basaltImportedNames(codeText: string): ReadonlySet<string> {
439
+ const out = new Set<string>()
440
+ for (const m of codeText.matchAll(BASALT_NAMED_IMPORT)) {
441
+ for (const raw of (m[1] as string).split(',')) {
442
+ const spec = raw.trim().replace(/^type\s+/, '')
443
+ if (spec.length === 0) continue
444
+ const parts = spec.split(/\s+as\s+/)
445
+ const bound = (parts[parts.length - 1] as string).trim()
446
+ if (bound.length > 0) out.add(bound)
447
+ }
448
+ }
449
+ return out
450
+ }
451
+
406
452
  // Raw lowercase form-control element — line-scoped, same shape as RAW_VISX_AXIS. `\b` after the
407
453
  // tag name rejects a same-prefixed custom component (`<inputRef`, `<selectAll`).
408
454
  const RAW_FORM_CONTROL = /<(?:input|select|textarea)\b/g
@@ -2047,10 +2093,25 @@ export function checkSource(text: string, relPath: string, cfg: GuardConfig): Fi
2047
2093
  }
2048
2094
  }
2049
2095
 
2096
+ // Both chart tag rules below gate on tag PROVENANCE (see the block above LOCAL_COMPONENT_DEF).
2097
+ // Computed once, and only if one of the two actually runs.
2098
+ const unframedChartRuns =
2099
+ GUARD_RULES['unframed-chart'].enabled!(cfg) && ruleApplies('unframed-chart', relPath)
2100
+ const ariaLabelRuns =
2101
+ GUARD_RULES['chart-missing-aria-label'].enabled!(cfg) &&
2102
+ ruleApplies('chart-missing-aria-label', relPath)
2103
+ const localNames =
2104
+ unframedChartRuns || ariaLabelRuns ? localComponentNames(codeText) : new Set<string>()
2105
+ const basaltNames =
2106
+ unframedChartRuns || ariaLabelRuns ? basaltImportedNames(codeText) : new Set<string>()
2107
+ const isShippedTag = (tag: string): boolean => basaltNames.has(tag) || !localNames.has(tag)
2108
+
2050
2109
  // unframed-chart — full-text tag-scoped scan (not per-line, see RAW_CHART_LEGEND_ARRAY comment).
2051
2110
  // Scans `codeText` (comment-stripped) so a legend example inside a comment can't match; reports
2052
- // at the line of the `items={[` token itself.
2053
- if (GUARD_RULES['unframed-chart'].enabled!(cfg) && ruleApplies('unframed-chart', relPath)) {
2111
+ // at the line of the `items={[` token itself. Skipped wholesale when `ChartLegend` is this file's
2112
+ // OWN component: a consumer's legend taking an `items` array is not the shipped one that derives
2113
+ // its entries, and the remedy ("pass deriveLegend(series)") is unreachable there.
2114
+ if (unframedChartRuns && isShippedTag('ChartLegend')) {
2054
2115
  for (const m of codeText.matchAll(RAW_CHART_LEGEND_ARRAY)) {
2055
2116
  const lineNo = codeText.slice(0, (m.index ?? 0) + m[0].length).split('\n').length
2056
2117
  push('unframed-chart', lineNo, 'items={[')
@@ -2064,12 +2125,11 @@ export function checkSource(text: string, relPath: string, cfg: GuardConfig): Fi
2064
2125
  // which is what `Finding.text` would then quote back — the closing bracket rather than the tag
2065
2126
  // that is missing the prop. The allow-comment is honored anywhere in the tag's span, so a
2066
2127
  // `theme-allow` that used to sit on the closing line keeps working.
2067
- if (
2068
- GUARD_RULES['chart-missing-aria-label'].enabled!(cfg) &&
2069
- ruleApplies('chart-missing-aria-label', relPath)
2070
- ) {
2128
+ if (ariaLabelRuns) {
2071
2129
  for (const m of codeText.matchAll(CHART_ENTRY_POINT_TAG)) {
2072
2130
  const tagText = m[0]
2131
+ // A tag this file defines itself is not the shipped kind — it does not take `ariaLabel`.
2132
+ if (!isShippedTag(m[1] as string)) continue
2073
2133
  if (HAS_ARIA_LABEL_PROP.test(tagText)) continue
2074
2134
  const startLine = codeText.slice(0, m.index ?? 0).split('\n').length
2075
2135
  const endLine = startLine + (tagText.split('\n').length - 1)