Skip to content

💅 lint/a11y/useAnchorContent: the PascalCase component allowance is applied only to self-closing elements #11872

Description

@necofuryai

Environment information

Details
CLI:
  Version:                      2.5.14
  Color support:                false

Platform:
  CPU Architecture:             aarch64
  OS:                           macos

Environment:
  BIOME_DISTRIBUTION:                npm
  BIOME_LOG_FILE:                    unset
  BIOME_LOG_PATH:                    unset
  BIOME_LOG_PREFIX_NAME:             unset
  BIOME_LOG_LEVEL:                   unset
  BIOME_LOG_KIND:                    unset
  BIOME_CONFIG_PATH:                 unset
  BIOME_THREADS:                     unset
  BIOME_WATCHER_KIND:                unset
  BIOME_WATCHER_POLLING_INTERVAL:    unset
  BIOME_BINARY:                      unset
  RUST_BACKTRACE:                    unset
  NO_COLOR:                     unset
  TERM:                         unset
  JS_RUNTIME_VERSION:           v24.21.0
  JS_RUNTIME_NAME:              node
  NODE_PACKAGE_MANAGER:         pnpm/10.30.1

Biome Configuration:
  Status:                       Loaded successfully.
  Path:                         biome.json
  Formatter enabled:            true
  Linter enabled:               true
  Assist enabled:               true
  VCS enabled:                  false
  HTML full support enabled:    true

Linter:
  JavaScript enabled:           unset
  JSON enabled:                 unset
  CSS enabled:                  unset
  GraphQL enabled:              unset
  Recommended:                  unset
  Enabled rules:
    a11y/noAccessKey
    a11y/noAmbiguousAnchorText
    a11y/noAriaHiddenOnFocusable
    a11y/noAriaUnsupportedElements
    a11y/noAutofocus
    a11y/noDistractingElements
    a11y/noHeaderScope
    a11y/noInteractiveElementToNoninteractiveRole
    a11y/noLabelWithoutControl
    a11y/noNoninteractiveElementToInteractiveRole
    a11y/noNoninteractiveTabindex
    a11y/noPositiveTabindex
    a11y/noRedundantAlt
    a11y/noRedundantRoles
    a11y/noStaticElementInteractions
    a11y/noSvgWithoutTitle
    a11y/useAltText
    a11y/useAnchorContent
    a11y/useAriaActivedescendantWithTabindex
    a11y/useAriaPropsForRole
    a11y/useAriaPropsSupportedByRole
    a11y/useButtonType
    a11y/useFocusableInteractive
    a11y/useGenericFontNames
    a11y/useHeadingContent
    a11y/useHtmlLang
    a11y/useIframeTitle
    a11y/useKeyWithClickEvents
    a11y/useKeyWithMouseEvents
    a11y/useMediaCaption
    a11y/useSemanticElements
    a11y/useValidAnchor
    a11y/useValidAriaProps
    a11y/useValidAriaRole
    a11y/useValidAriaValues
    a11y/useValidAutocomplete
    a11y/useValidLang
    complexity/noAdjacentSpacesInRegex
    complexity/noArguments
    complexity/noBannedTypes
    complexity/noCommaOperator
    complexity/noEmptyTypeParameters
    complexity/noExtraBooleanCast
    complexity/noFlatMapIdentity
    complexity/noImportantStyles
    complexity/noStaticOnlyClass
    complexity/noThisInStatic
    complexity/noUselessCatch
    complexity/noUselessConstructor
    complexity/noUselessContinue
    complexity/noUselessEmptyExport
    complexity/noUselessEscapeInRegex
    complexity/noUselessFragments
    complexity/noUselessLabel
    complexity/noUselessLoneBlockStatements
    complexity/noUselessRename
    complexity/noUselessStringRaw
    complexity/noUselessSwitchCase
    complexity/noUselessTernary
    complexity/noUselessThisAlias
    complexity/noUselessTypeConstraint
    complexity/noUselessUndefinedInitialization
    complexity/useArrowFunction
    complexity/useDateNow
    complexity/useFlatMap
    complexity/useIndexOf
    complexity/useLiteralKeys
    complexity/useNumericLiterals
    complexity/useOptionalChain
    complexity/useRegexLiterals
    complexity/useSimpleNumberKeys
    correctness/noConstAssign
    correctness/noConstantCondition
    correctness/noConstantMathMinMaxClamp
    correctness/noConstructorReturn
    correctness/noDuplicateArgumentNames
    correctness/noDuplicateAttributes
    correctness/noDuplicateEnumValueNames
    correctness/noDuplicateInputFieldNames
    correctness/noDuplicateVariableNames
    correctness/noEmptyCharacterClassInRegex
    correctness/noEmptyPattern
    correctness/noGlobalObjectCalls
    correctness/noInnerDeclarations
    correctness/noInvalidBuiltinInstantiation
    correctness/noInvalidConstructorSuper
    correctness/noInvalidDirectionInLinearGradient
    correctness/noInvalidGridAreas
    correctness/noInvalidPositionAtImportRule
    correctness/noInvalidUseBeforeDeclaration
    correctness/noMissingVarFunction
    correctness/noNonoctalDecimalEscape
    correctness/noPrecisionLoss
    correctness/noSelfAssign
    correctness/noSetterReturn
    correctness/noStringCaseMismatch
    correctness/noSwitchDeclarations
    correctness/noUnknownFunction
    correctness/noUnknownMediaFeatureName
    correctness/noUnknownProperty
    correctness/noUnknownPseudoClass
    correctness/noUnknownPseudoElement
    correctness/noUnknownTypeSelector
    correctness/noUnknownUnit
    correctness/noUnmatchableAnbSelector
    correctness/noUnreachable
    correctness/noUnreachableSuper
    correctness/noUnsafeFinally
    correctness/noUnsafeOptionalChaining
    correctness/noUnusedFunctionParameters
    correctness/noUnusedImports
    correctness/noUnusedLabels
    correctness/noUnusedPrivateClassMembers
    correctness/noUnusedVariables
    correctness/noVoidElementsWithChildren
    correctness/noVoidTypeReturn
    correctness/useGraphqlNamedOperations
    correctness/useIsNan
    correctness/useLoneAnonymousOperation
    correctness/useParseIntRadix
    correctness/useValidForDirection
    correctness/useValidTypeof
    correctness/useYield
    performance/noAccumulatingSpread
    performance/noDynamicNamespaceImportAccess
    security/noBlankTarget
    security/noGlobalEval
    security/noScriptUrl
    style/noDescendingSpecificity
    style/noNonNullAssertion
    style/useArrayLiterals
    style/useConst
    style/useDeprecatedReason
    style/useExponentiationOperator
    style/useExportType
    style/useImportType
    style/useLiteralEnumMembers
    style/useNodejsImportProtocol
    style/useShorthandFunctionType
    style/useTemplate
    suspicious/noApproximativeNumericConstant
    suspicious/noAssignInExpressions
    suspicious/noAsyncPromiseExecutor
    suspicious/noBiomeFirstException
    suspicious/noCatchAssign
    suspicious/noClassAssign
    suspicious/noCommentText
    suspicious/noCompareNegZero
    suspicious/noConfusingLabels
    suspicious/noConfusingVoidType
    suspicious/noConstEnum
    suspicious/noControlCharactersInRegex
    suspicious/noDebugger
    suspicious/noDeprecatedMediaType
    suspicious/noDocumentCookie
    suspicious/noDoubleEquals
    suspicious/noDuplicateAtImportRules
    suspicious/noDuplicateCase
    suspicious/noDuplicateClassMembers
    suspicious/noDuplicateCustomProperties
    suspicious/noDuplicateElseIf
    suspicious/noDuplicateEnumValues
    suspicious/noDuplicateFields
    suspicious/noDuplicateFontNames
    suspicious/noDuplicateJsxProps
    suspicious/noDuplicateObjectKeys
    suspicious/noDuplicateParameters
    suspicious/noDuplicateProperties
    suspicious/noDuplicateSelectorsKeyframeBlock
    suspicious/noEmptyBlock
    suspicious/noEmptyInterface
    suspicious/noExplicitAny
    suspicious/noExtraNonNullAssertion
    suspicious/noFallthroughSwitchClause
    suspicious/noFunctionAssign
    suspicious/noGlobalAssign
    suspicious/noGlobalIsFinite
    suspicious/noGlobalIsNan
    suspicious/noImplicitAnyLet
    suspicious/noImportAssign
    suspicious/noImportantInKeyframe
    suspicious/noIrregularWhitespace
    suspicious/noLabelVar
    suspicious/noMisleadingCharacterClass
    suspicious/noMisleadingInstantiator
    suspicious/noMisrefactoredShorthandAssign
    suspicious/noNonNullAssertedOptionalChain
    suspicious/noOctalEscape
    suspicious/noProto
    suspicious/noPrototypeBuiltins
    suspicious/noQuickfixBiome
    suspicious/noRedeclare
    suspicious/noRedundantUseStrict
    suspicious/noSelfCompare
    suspicious/noShadowRestrictedNames
    suspicious/noShorthandPropertyOverrides
    suspicious/noSparseArray
    suspicious/noSuspiciousSemicolonInJsx
    suspicious/noTemplateCurlyInString
    suspicious/noThenProperty
    suspicious/noTsIgnore
    suspicious/noUnknownAtRules
    suspicious/noUnsafeDeclarationMerging
    suspicious/noUnsafeNegation
    suspicious/noUselessEscapeInString
    suspicious/noUselessRegexBackrefs
    suspicious/noWith
    suspicious/useAdjacentOverloadSignatures
    suspicious/useBiomeIgnoreFolder
    suspicious/useDefaultSwitchClauseLast
    suspicious/useGetterReturn
    suspicious/useGoogleFontDisplay
    suspicious/useIsArray
    suspicious/useIterableCallbackReturn
    suspicious/useNamespaceKeyword

Workspace:
  Open Documents:               0

Rule name

lint/a11y/useAnchorContent

Playground link

https://biomejs.dev/playground/?pane=Diagnostics&language=astro#code=PABhACAAaAByAGUAZgA9ACIALwB4ACIAPgA8AEkAYwBvAG4AIAAvAD4APAAvAGEAPgAKADwAYQAgAGgAcgBlAGYAPQAiAC8AeQAiAD4APABJAGMAbwBuAD4APAAvAEkAYwBvAG4APgA8AC8AYQA%2BAAoA

Expected result

useAnchorContent already treats a PascalCase component as possible accessible content, but only when the component is written as a self-closing element. Written with an explicit closing tag — the same thing in Astro, Vue and Svelte — the anchor is reported as empty.

main.astro:

<a href="/x"><Icon /></a>
<a href="/y"><Icon></Icon></a>
$ biome lint main.astro
main.astro:2:1 lint/a11y/useAnchorContent ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Provide screen reader accessible content when using a elements.

Found 1 error.

Only line 2 is reported. Biome cannot know what <Icon> renders in either spelling, so I would expect neither line to be reported — and in any case I would expect the two spellings to be treated the same.

The equivalent JSX reports neither spelling:

export const A = () => <a href="/x"><Icon /></a>;
export const B = () => <a href="/y"><Icon></Icon></a>;
$ biome lint main.jsx
Checked 1 file in 1191µs. No fixes applied.

The changelog entry for #11790 gives "matching JSX behavior" as the goal, so the difference looks unintended.

This is not a regression: the asymmetry behaves the same in 2.5.13 and still reproduces on next (build 137d48a). What changed in 2.5.14 is the removal of the early return for anchors that have an accessible name (#11790), which used to mask it:

main.astro above 2.5.13 2.5.14
as written 1 error 1 error
with aria-label on both anchors 0 errors 1 error
Where it comes from

In crates/biome_html_analyze/src/lint/a11y/use_anchor_content.rs, has_accessible_content handles the two element kinds differently. The self-closing branch inspects the tag name:

AnyHtmlElement::HtmlSelfClosingElement(element) => {
    // ...
    match tag_text.as_ref().map(|t| t.as_ref()) {
        // ...
        // Custom components (PascalCase) may render accessible content
        Some(name) if name.starts_with(|c: char| c.is_uppercase()) => true,
        _ => false,
    }
}

The paired-tag branch never looks at the name and recurses into the children, so a component with no children evaluates to false:

AnyHtmlElement::HtmlElement(element) => {
    if html_element_has_truthy_aria_hidden(element) {
        false
    } else {
        has_accessible_content(&element.children(), is_astro)
    }
}

The Astro Image case added in #9270 / #9369 lives in the same self-closing branch, so <a><Image alt="x"></Image></a> is reported while <a><Image alt="x" /></a> is not.

Related

#10663 / #10699 report and fix the same shape of asymmetry for <slot>, adding the check to both branches. #9270 / #9369 added the Astro Image case to the self-closing branch. This issue is about the PascalCase component case, which the paired-tag branch still misses.

Code of Conduct

  • I agree to follow Biome's Code of Conduct

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-LinterArea: linterL-HTMLLanguage: HTML and super languagesS-Bug-confirmedStatus: report has been confirmed as a valid bug

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions