From 09d3661e20b760ad05e2f80d1012aa642502d5e9 Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Thu, 26 Feb 2026 18:14:06 +0100 Subject: [PATCH 1/5] fix: Refactor of in checks --- src/property-filter/controller.ts | 3 ++- src/property-filter/filter-options.ts | 3 ++- src/property-filter/interfaces.ts | 16 ++++++++++++++++ src/property-filter/internal.tsx | 6 +++++- src/property-filter/token.tsx | 12 +++++++++--- src/property-filter/utils.ts | 9 +++++++-- 6 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/property-filter/controller.ts b/src/property-filter/controller.ts index 5deb1b480e..2ac70b1fe5 100644 --- a/src/property-filter/controller.ts +++ b/src/property-filter/controller.ts @@ -14,6 +14,7 @@ import { InternalQuery, InternalToken, InternalTokenGroup, + isInternalToken, JoinOperation, ParsedText, Query, @@ -46,7 +47,7 @@ export const getQueryActions = ({ }) => { const setQuery = (query: InternalQuery) => { function transformToken(token: InternalToken | InternalTokenGroup): Token | TokenGroup { - if ('operator' in token) { + if (isInternalToken(token)) { return matchTokenValue(token, filteringOptions); } return { ...token, tokens: token.tokens.map(transformToken) }; diff --git a/src/property-filter/filter-options.ts b/src/property-filter/filter-options.ts index 4fefef4390..46d4422c5e 100644 --- a/src/property-filter/filter-options.ts +++ b/src/property-filter/filter-options.ts @@ -27,7 +27,8 @@ export function filterOptions( } function isGroup(optionOrGroup: AutosuggestProps.Option): optionOrGroup is AutosuggestProps.OptionGroup { - return 'options' in optionOrGroup; + const key: keyof AutosuggestProps.OptionGroup = 'options'; + return key in optionOrGroup; } function matchSingleOption(option: OptionDefinition, searchText: string): boolean { diff --git a/src/property-filter/interfaces.ts b/src/property-filter/interfaces.ts index 548af4bad1..bf9b8cddb2 100644 --- a/src/property-filter/interfaces.ts +++ b/src/property-filter/interfaces.ts @@ -416,3 +416,19 @@ export type ParsedText = | { step: 'property'; property: InternalFilteringProperty; operator: ComparisonOperator; value: string } | { step: 'operator'; property: InternalFilteringProperty; operatorPrefix: string } | { step: 'free-text'; operator?: ComparisonOperator; value: string }; + +// Type guards for InternalToken | InternalTokenGroup union discrimination. +// Using keyof ensures compile-time validation of the discriminant property name. +// See AWSUI-59006 for context. + +export function isInternalToken(tokenOrGroup: InternalToken | InternalTokenGroup): tokenOrGroup is InternalToken { + const key: keyof InternalToken = 'operator'; + return key in tokenOrGroup; +} + +export function isInternalTokenGroup( + tokenOrGroup: InternalToken | InternalTokenGroup +): tokenOrGroup is InternalTokenGroup { + const key: keyof InternalTokenGroup = 'operation'; + return key in tokenOrGroup; +} diff --git a/src/property-filter/internal.tsx b/src/property-filter/internal.tsx index 86cf0ca3d3..abb9508251 100644 --- a/src/property-filter/internal.tsx +++ b/src/property-filter/internal.tsx @@ -161,7 +161,11 @@ const PropertyFilterInternal = React.forwardRef( tokenOrGroup: Token | TokenGroup, standaloneIndex?: number ): InternalToken | InternalTokenGroup { - return 'operation' in tokenOrGroup + const isTokenGroup = (t: Token | TokenGroup): t is TokenGroup => { + const key: keyof TokenGroup = 'operation'; + return key in t; + }; + return isTokenGroup(tokenOrGroup) ? { operation: tokenOrGroup.operation, tokens: tokenOrGroup.tokens.map(token => transformToken(token)), diff --git a/src/property-filter/token.tsx b/src/property-filter/token.tsx index 995d867e89..21a233278a 100644 --- a/src/property-filter/token.tsx +++ b/src/property-filter/token.tsx @@ -19,6 +19,8 @@ import { InternalQuery, InternalToken, InternalTokenGroup, + isInternalToken, + isInternalTokenGroup, JoinOperation, LoadItemsDetail, } from './interfaces'; @@ -71,20 +73,24 @@ export const TokenButton = ({ }: TokenProps) => { const tokenRef = useRef(null); - const hasGroups = query.tokens.some(tokenOrGroup => 'operation' in tokenOrGroup); + const hasGroups = query.tokens.some(isInternalTokenGroup); const first = tokenIndex === 0; const tokenOrGroup = query.tokens[tokenIndex]; const tokens = tokenGroupToTokens([tokenOrGroup]).map(t => ({ ...t, standaloneIndex: undefined })); const operation = query.operation; - const groupOperation = 'operation' in tokenOrGroup ? tokenOrGroup.operation : operation === 'and' ? 'or' : 'and'; + const groupOperation = isInternalTokenGroup(tokenOrGroup) + ? tokenOrGroup.operation + : operation === 'and' + ? 'or' + : 'and'; const [tempTokens, setTempTokens] = useState(tokens); const capturedTokenIndices = tempTokens.map(token => token.standaloneIndex).filter(index => index !== undefined); const tokensToCapture: InternalToken[] = []; for (let index = 0; index < query.tokens.length; index++) { const token = query.tokens[index]; - if ('operator' in token && token !== tokenOrGroup && !capturedTokenIndices.includes(index)) { + if (isInternalToken(token) && token !== tokenOrGroup && !capturedTokenIndices.includes(index)) { tokensToCapture.push(token); } } diff --git a/src/property-filter/utils.ts b/src/property-filter/utils.ts index fea663011b..fbc31adac9 100644 --- a/src/property-filter/utils.ts +++ b/src/property-filter/utils.ts @@ -137,17 +137,22 @@ interface AbstractTokenGroup { tokens: readonly (T | AbstractTokenGroup)[]; } +function isAbstractToken(tokenOrGroup: T | AbstractTokenGroup): tokenOrGroup is T { + const key: keyof AbstractToken = 'operator'; + return key in tokenOrGroup; +} + /** * Transforms query token groups to tokens (only taking 1 level of nesting). */ export function tokenGroupToTokens(tokenGroups: readonly (T | AbstractTokenGroup)[]): T[] { const tokens: T[] = []; for (const tokenOrGroup of tokenGroups) { - if ('operator' in tokenOrGroup) { + if (isAbstractToken(tokenOrGroup)) { tokens.push(tokenOrGroup); } else { for (const nestedTokenOrGroup of tokenOrGroup.tokens) { - if ('operator' in nestedTokenOrGroup) { + if (isAbstractToken(nestedTokenOrGroup)) { tokens.push(nestedTokenOrGroup); } else { // Ignore deeply nested tokens From e9eb6de275ac156c5ebb56da178c22412e6fd3ca Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Thu, 26 Feb 2026 18:24:40 +0100 Subject: [PATCH 2/5] fix: Refactor of in checks --- src/autosuggest/autosuggest-option.tsx | 6 +++--- src/autosuggest/options-controller.ts | 3 ++- src/autosuggest/utils/utils.ts | 2 +- src/internal/components/option/utils/filter-options.ts | 6 ++++-- src/select/utils/check-option-value-field.ts | 2 ++ 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/autosuggest/autosuggest-option.tsx b/src/autosuggest/autosuggest-option.tsx index 2bf0506788..ba7682acb0 100644 --- a/src/autosuggest/autosuggest-option.tsx +++ b/src/autosuggest/autosuggest-option.tsx @@ -108,9 +108,9 @@ const AutosuggestOption = ( ref: React.Ref ) => { const baseProps = getBaseProps(rest); - const useEntered = 'type' in option && option.type === 'use-entered'; - const isParent = 'type' in option && option.type === 'parent'; - const isChild = 'type' in option && option.type === 'child'; + const useEntered = option.type === 'use-entered'; + const isParent = option.type === 'parent'; + const isChild = option.type === 'child'; const { throughIndex, inGroupIndex, groupIndex } = getTestOptionIndexes(option) || {}; let optionContent; diff --git a/src/autosuggest/options-controller.ts b/src/autosuggest/options-controller.ts index abb94f3dba..73cbc1a94b 100644 --- a/src/autosuggest/options-controller.ts +++ b/src/autosuggest/options-controller.ts @@ -179,5 +179,6 @@ function createItems(options: Options) { } function isGroup(optionOrGroup: AutosuggestProps.Option): optionOrGroup is AutosuggestProps.OptionGroup { - return 'options' in optionOrGroup; + const key: keyof AutosuggestProps.OptionGroup = 'options'; + return key in optionOrGroup; } diff --git a/src/autosuggest/utils/utils.ts b/src/autosuggest/utils/utils.ts index 5c4bbff50b..0cc6082f49 100644 --- a/src/autosuggest/utils/utils.ts +++ b/src/autosuggest/utils/utils.ts @@ -5,7 +5,7 @@ import { AutosuggestItem } from '../interfaces'; type SearchableFields = 'value' | 'label' | 'description' | 'labelTag'; type SearchableTagFields = 'tags' | 'filteringTags'; -const isGroup = (option: AutosuggestItem) => 'type' in option && option.type === 'parent'; +const isGroup = (option: AutosuggestItem) => option.type === 'parent'; const popLastGroup = (options: AutosuggestItem[]) => { if (options.length) { diff --git a/src/internal/components/option/utils/filter-options.ts b/src/internal/components/option/utils/filter-options.ts index ff4d492218..8e7b701169 100644 --- a/src/internal/components/option/utils/filter-options.ts +++ b/src/internal/components/option/utils/filter-options.ts @@ -73,5 +73,7 @@ export const isInteractive = (option?: DropdownOption) => !!option && !option.di export const isGroupInteractive = (option?: DropdownOption) => !!option && !option.disabled; -export const isGroup = (option?: OptionDefinition | OptionGroup): option is OptionGroup => - !!option && 'options' in option && !!option.options; +export const isGroup = (option?: OptionDefinition | OptionGroup): option is OptionGroup => { + const key: keyof OptionGroup = 'options'; + return !!option && key in option && !!option.options; +}; diff --git a/src/select/utils/check-option-value-field.ts b/src/select/utils/check-option-value-field.ts index 2622358713..6134e101b0 100644 --- a/src/select/utils/check-option-value-field.ts +++ b/src/select/utils/check-option-value-field.ts @@ -14,6 +14,8 @@ export function checkOptionValueField>( return; } + // The `in` checks below are intentional: `element` is `any` (loosely-typed external input), + // so compile-time `keyof` validation is not possible. These checks validate runtime shape. const valuePropertyMissing = !propertyValue.every(element => { return 'options' in element || 'value' in element; }); From 8909d7e60a9a3ea9627abcf36e84c26e5b07a604 Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Thu, 26 Feb 2026 18:52:27 +0100 Subject: [PATCH 3/5] fix: Refactor of in checks --- src/button-dropdown/internal.tsx | 10 ++++++---- src/button-group/item-element.tsx | 3 ++- src/flashbar/collapsible-flashbar.tsx | 12 +++++------- src/flashbar/non-collapsible-flashbar.tsx | 7 ++----- src/flashbar/utils.ts | 12 ++++++++++++ src/mixed-line-bar-chart/utils.ts | 6 ++++-- src/side-navigation/util.tsx | 6 ++++-- src/top-navigation/parts/utility.tsx | 4 +++- 8 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/button-dropdown/internal.tsx b/src/button-dropdown/internal.tsx index e616784397..b392d6282d 100644 --- a/src/button-dropdown/internal.tsx +++ b/src/button-dropdown/internal.tsx @@ -22,7 +22,7 @@ import { GeneratedAnalyticsMetadataButtonDropdownCollapse, GeneratedAnalyticsMetadataButtonDropdownExpand, } from './analytics-metadata/interfaces.js'; -import { ButtonDropdownProps, InternalButtonDropdownProps } from './interfaces'; +import { ButtonDropdownProps, InternalButtonDropdownProps, InternalItem } from './interfaces'; import ItemsList from './items-list'; import { useButtonDropdown } from './utils/use-button-dropdown'; import { isLinkItem } from './utils/utils.js'; @@ -180,18 +180,20 @@ const InternalButtonDropdown = React.forwardRef( const triggerId = useUniqueId('awsui-button-dropdown__trigger'); const triggerHasBadge = () => { + const groupKey: keyof ButtonDropdownProps.ItemGroup = 'items'; const flatItems = items.flatMap(item => { - if ('items' in item) { + if (groupKey in item) { return item.items; } return item; }); + const badgeKey: keyof InternalItem = 'badge'; return ( variant === 'icon' && !!flatItems?.find(item => { - if ('badge' in item) { - return item.badge; + if (badgeKey in item) { + return (item as InternalItem).badge; } }) ); diff --git a/src/button-group/item-element.tsx b/src/button-group/item-element.tsx index 8cc623e1c1..325dd7e538 100644 --- a/src/button-group/item-element.tsx +++ b/src/button-group/item-element.tsx @@ -103,7 +103,8 @@ const ItemElement = forwardRef( }; const onClickHandler = (event: CustomEvent) => { - const hasPopoverFeedback = 'popoverFeedback' in item && item.popoverFeedback; + const feedbackKey: keyof ButtonGroupProps.IconButton = 'popoverFeedback'; + const hasPopoverFeedback = feedbackKey in item && item.popoverFeedback; if (hasPopoverFeedback) { setTooltip({ item: item.id, feedback: true }); diff --git a/src/flashbar/collapsible-flashbar.tsx b/src/flashbar/collapsible-flashbar.tsx index 60d10a51e6..644ac35f1b 100644 --- a/src/flashbar/collapsible-flashbar.tsx +++ b/src/flashbar/collapsible-flashbar.tsx @@ -35,6 +35,8 @@ import { getFlashTypeCount, getItemColor, getVisibleCollapsedItems, + isRefObject, + isStackableItem, StackableItem, } from './utils'; @@ -214,11 +216,11 @@ export default function CollapsibleFlashbar({ items, style, ...restProps }: Inte // we need to use different, more custom and more controlled animations. const hasEntered = (item: StackableItem | FlashbarProps.MessageDefinition) => enteringItems.some(_item => _item.id && _item.id === item.id); - const hasLeft = (item: StackableItem | FlashbarProps.MessageDefinition) => !('expandedIndex' in item); + const hasLeft = (item: StackableItem | FlashbarProps.MessageDefinition) => !isStackableItem(item); const hasEnteredOrLeft = (item: StackableItem | FlashbarProps.MessageDefinition) => hasEntered(item) || hasLeft(item); const showInnerContent = (item: StackableItem | FlashbarProps.MessageDefinition) => - isFlashbarStackExpanded || hasLeft(item) || ('expandedIndex' in item && item.expandedIndex === 0); + isFlashbarStackExpanded || hasLeft(item) || (isStackableItem(item) && item.expandedIndex === 0); const shouldUseStandardAnimation = (item: StackableItem, index: number) => index === 0 && hasEnteredOrLeft(item); @@ -301,11 +303,7 @@ export default function CollapsibleFlashbar({ items, style, ...restProps }: Inte if (shouldUseStandardAnimation(item, index) && transitionRootElement) { if (typeof transitionRootElement === 'function') { transitionRootElement(el); - } else if ( - transitionRootElement && - typeof transitionRootElement === 'object' && - 'current' in transitionRootElement - ) { + } else if (isRefObject(transitionRootElement)) { (transitionRootElement as React.MutableRefObject).current = el; } } diff --git a/src/flashbar/non-collapsible-flashbar.tsx b/src/flashbar/non-collapsible-flashbar.tsx index 96cb0015b0..6c33ddb876 100644 --- a/src/flashbar/non-collapsible-flashbar.tsx +++ b/src/flashbar/non-collapsible-flashbar.tsx @@ -13,6 +13,7 @@ import { useFlashbar, useFlashbarVisibility } from './common'; import { TIMEOUT_FOR_ENTERING_ANIMATION } from './constant'; import { Flash } from './flash'; import { FlashbarProps, InternalFlashbarProps } from './interfaces'; +import { isRefObject } from './utils'; import styles from './styles.css.js'; @@ -121,11 +122,7 @@ export default function NonCollapsibleFlashbar({ items, i18nStrings, style, ...r // If there's a transition root element ref, update it too if (transitionRootElement && typeof transitionRootElement === 'function') { transitionRootElement(el); - } else if ( - transitionRootElement && - typeof transitionRootElement === 'object' && - 'current' in transitionRootElement - ) { + } else if (transitionRootElement && isRefObject(transitionRootElement)) { (transitionRootElement as React.MutableRefObject).current = el; } }} diff --git a/src/flashbar/utils.ts b/src/flashbar/utils.ts index efbc35aa22..8fbf33029c 100644 --- a/src/flashbar/utils.ts +++ b/src/flashbar/utils.ts @@ -1,5 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + import { IconProps } from '../icon/interfaces'; import { FlashbarProps } from './interfaces'; @@ -13,6 +15,16 @@ export interface StackableItem extends FlashbarProps.MessageDefinition { collapsedIndex?: number; } +export function isStackableItem(item: StackableItem | FlashbarProps.MessageDefinition): item is StackableItem { + const key: keyof StackableItem = 'expandedIndex'; + return key in item; +} + +export function isRefObject(ref: React.Ref): ref is React.RefObject { + const key: keyof React.RefObject = 'current'; + return ref !== null && typeof ref === 'object' && key in ref; +} + const typesToColors: Record = { error: 'red', info: 'blue', diff --git a/src/mixed-line-bar-chart/utils.ts b/src/mixed-line-bar-chart/utils.ts index 0baa16f70c..3f7216c0c5 100644 --- a/src/mixed-line-bar-chart/utils.ts +++ b/src/mixed-line-bar-chart/utils.ts @@ -100,13 +100,15 @@ export const getKeyValue = (key: ChartDataTypes) => (key instanceof Date ? key.g export function isYThreshold( series: MixedLineBarChartProps.ChartSeries ): series is MixedLineBarChartProps.YThresholdSeries { - return series.type === 'threshold' && 'y' in series; + const key: keyof MixedLineBarChartProps.YThresholdSeries = 'y'; + return series.type === 'threshold' && key in series; } export function isXThreshold( series: MixedLineBarChartProps.ChartSeries ): series is MixedLineBarChartProps.XThresholdSeries { - return series.type === 'threshold' && 'x' in series; + const key: keyof MixedLineBarChartProps.XThresholdSeries = 'x'; + return series.type === 'threshold' && key in series; } export function isDataSeries( diff --git a/src/side-navigation/util.tsx b/src/side-navigation/util.tsx index 8f74d5543a..e6750833d0 100644 --- a/src/side-navigation/util.tsx +++ b/src/side-navigation/util.tsx @@ -50,14 +50,16 @@ export function checkDuplicateHrefs(items: ReadonlyArray Date: Fri, 27 Feb 2026 14:54:25 +0100 Subject: [PATCH 4/5] fix: Refactor of in checks --- eslint.config.mjs | 7 +++++++ src/app-layout/runtime-drawer/index.tsx | 2 ++ .../visual-refresh-toolbar/state/use-app-layout.tsx | 1 + src/flashbar/common.tsx | 1 + src/internal/components/focus-lock/utils.ts | 1 + src/internal/utils/dom.ts | 5 +++++ src/link/internal.tsx | 1 + src/navigable-group/internal.tsx | 1 + src/property-filter/internal.tsx | 1 + src/tabs/native-smooth-scroll-supported.ts | 1 + src/test-utils/dom/code-editor/index.ts | 1 + 11 files changed, 22 insertions(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index ee54c8c7ce..de9f3011a1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -115,6 +115,13 @@ export default tsEslint.config( 'line', [' Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.', ' SPDX-License-Identifier: Apache-2.0'], ], + 'no-restricted-syntax': [ + 'error', + { + selector: 'BinaryExpression[operator="in"][left.type="Literal"]', + message: 'Prefer a type guard function with `keyof` instead of raw `in` checks. See AWSUI-59006.', + }, + ], 'no-warning-comments': 'warn', 'simple-import-sort/imports': [ 'error', diff --git a/src/app-layout/runtime-drawer/index.tsx b/src/app-layout/runtime-drawer/index.tsx index 9638f378ee..009d8f2826 100644 --- a/src/app-layout/runtime-drawer/index.tsx +++ b/src/app-layout/runtime-drawer/index.tsx @@ -87,10 +87,12 @@ function mapRuntimeHeaderActionsToHeaderActions( return runtimeHeaderActions.map(runtimeHeaderAction => { return { ...runtimeHeaderAction, + // eslint-disable-next-line no-restricted-syntax -- Runtime plugin API: property not in TS type ...('iconSvg' in runtimeHeaderAction && runtimeHeaderAction.iconSvg && { iconSvg: convertRuntimeTriggerToReactNode(runtimeHeaderAction.iconSvg), }), + // eslint-disable-next-line no-restricted-syntax -- Runtime plugin API: property not in TS type ...('pressedIconSvg' in runtimeHeaderAction && runtimeHeaderAction.pressedIconSvg && { iconSvg: convertRuntimeTriggerToReactNode(runtimeHeaderAction.pressedIconSvg), diff --git a/src/app-layout/visual-refresh-toolbar/state/use-app-layout.tsx b/src/app-layout/visual-refresh-toolbar/state/use-app-layout.tsx index d58868eb1e..aafb2b36c1 100644 --- a/src/app-layout/visual-refresh-toolbar/state/use-app-layout.tsx +++ b/src/app-layout/visual-refresh-toolbar/state/use-app-layout.tsx @@ -254,6 +254,7 @@ export const useAppLayout = ( return; } + // eslint-disable-next-line no-restricted-syntax -- postMessage validation: runtime data shape check if (!('payload' in message && 'id' in message.payload)) { metrics.sendOpsMetricObject('awsui-widget-drawer-incorrect-payload', { type: message.type, diff --git a/src/flashbar/common.tsx b/src/flashbar/common.tsx index adf4eab5b6..caf270bcf0 100644 --- a/src/flashbar/common.tsx +++ b/src/flashbar/common.tsx @@ -81,6 +81,7 @@ export function useFlashbar({ onItemsRemoved?: (items: FlashbarProps.MessageDefinition[]) => void; onItemsChanged?: (options?: { allItemsHaveId?: boolean; isReducedMotion?: boolean }) => void; }) { + // eslint-disable-next-line no-restricted-syntax -- Optional property existence check const allItemsHaveId = useMemo(() => items.every(item => 'id' in item), [items]); const baseProps = getBaseProps(restProps); const ref = useRef(null); diff --git a/src/internal/components/focus-lock/utils.ts b/src/internal/components/focus-lock/utils.ts index 014176e897..e12710f223 100644 --- a/src/internal/components/focus-lock/utils.ts +++ b/src/internal/components/focus-lock/utils.ts @@ -27,6 +27,7 @@ const tabbables = [ /** Whether the element or any of its ancestors are not hidden. */ function isVisible(element: HTMLElement): boolean { + // eslint-disable-next-line no-restricted-syntax -- Browser API feature detection if (!('checkVisibility' in element)) { // checkVisibility isn't defined in JSDOM. It's safer to assume everything is visible. return true; diff --git a/src/internal/utils/dom.ts b/src/internal/utils/dom.ts index b0d300d348..ebfcba4a44 100644 --- a/src/internal/utils/dom.ts +++ b/src/internal/utils/dom.ts @@ -64,10 +64,13 @@ export function isNode(target: unknown): target is Node { target instanceof Node || (target !== null && typeof target === 'object' && + // eslint-disable-next-line no-restricted-syntax -- Cross-window duck typing: instanceof fails across iframes 'nodeType' in target && typeof target.nodeType === 'number' && + // eslint-disable-next-line no-restricted-syntax -- Cross-window duck typing: instanceof fails across iframes 'nodeName' in target && typeof target.nodeName === 'string' && + // eslint-disable-next-line no-restricted-syntax -- Cross-window duck typing: instanceof fails across iframes 'parentNode' in target && typeof target.parentNode === 'object') ); @@ -78,6 +81,7 @@ export function isHTMLElement(target: unknown): target is HTMLElement { target instanceof HTMLElement || (isNode(target) && target.nodeType === Node.ELEMENT_NODE && + // eslint-disable-next-line no-restricted-syntax -- Cross-window HTMLElement detection 'style' in target && typeof target.style === 'object' && typeof target.ownerDocument === 'object' && @@ -90,6 +94,7 @@ export function isSVGElement(target: unknown): target is SVGElement { target instanceof SVGElement || (isNode(target) && target.nodeType === Node.ELEMENT_NODE && + // eslint-disable-next-line no-restricted-syntax -- Cross-window SVGElement detection 'ownerSVGElement' in target && typeof target.ownerSVGElement === 'object') ); diff --git a/src/link/internal.tsx b/src/link/internal.tsx index ccf9f38804..dce62bf611 100644 --- a/src/link/internal.tsx +++ b/src/link/internal.tsx @@ -129,6 +129,7 @@ const InternalLink = React.forwardRef( const fireClickEvent = (event: React.MouseEvent | React.KeyboardEvent) => { const { altKey, ctrlKey, metaKey, shiftKey } = event; + // eslint-disable-next-line no-restricted-syntax -- MouseEvent vs KeyboardEvent discrimination const button = 'button' in event ? event.button : 0; // make onClick non-cancelable to prevent it from being used to block full page reload // for navigation use `onFollow` event instead diff --git a/src/navigable-group/internal.tsx b/src/navigable-group/internal.tsx index cf40956263..4fa43b28fe 100644 --- a/src/navigable-group/internal.tsx +++ b/src/navigable-group/internal.tsx @@ -133,6 +133,7 @@ const InternalNavigableGroup = forwardRef( } function isElementDisabled(element: HTMLElement) { + // eslint-disable-next-line no-restricted-syntax -- DOM element capability check if ('disabled' in element) { return element.disabled; } diff --git a/src/property-filter/internal.tsx b/src/property-filter/internal.tsx index abb9508251..2e6ea1d496 100644 --- a/src/property-filter/internal.tsx +++ b/src/property-filter/internal.tsx @@ -320,6 +320,7 @@ const PropertyFilterInternal = React.forwardRef( return; } + // eslint-disable-next-line no-restricted-syntax -- Runtime property not in TS type if (!('keepOpenOnSelect' in option)) { createToken(value); return; diff --git a/src/tabs/native-smooth-scroll-supported.ts b/src/tabs/native-smooth-scroll-supported.ts index ed85293e31..3d99627834 100644 --- a/src/tabs/native-smooth-scroll-supported.ts +++ b/src/tabs/native-smooth-scroll-supported.ts @@ -3,5 +3,6 @@ // This function is in a separate file to allow for mocking in unit tests export default function () { + // eslint-disable-next-line no-restricted-syntax -- CSS feature detection return 'scrollBehavior' in document.documentElement.style; } diff --git a/src/test-utils/dom/code-editor/index.ts b/src/test-utils/dom/code-editor/index.ts index cddb435a01..1f8916934e 100644 --- a/src/test-utils/dom/code-editor/index.ts +++ b/src/test-utils/dom/code-editor/index.ts @@ -53,6 +53,7 @@ export default class CodeEditorWrapper extends ComponentWrapper { */ @usesDom setValue(value: string): void { const editor = this.findEditor()?.getElement() as any; + // eslint-disable-next-line no-restricted-syntax -- External library duck typing if (editor && 'env' in editor) { act(() => { editor.env.editor.setValue(value, -1); From 828c2d2325301c33f225d2923e01d8e8cd0799ae Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Wed, 4 Mar 2026 10:47:35 +0100 Subject: [PATCH 5/5] move from interface to utils --- src/property-filter/controller.ts | 2 +- src/property-filter/interfaces.ts | 16 ---------------- src/property-filter/token.tsx | 4 +--- src/property-filter/utils.ts | 13 +++++++++++++ 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/property-filter/controller.ts b/src/property-filter/controller.ts index 2ac70b1fe5..9d54ac4af1 100644 --- a/src/property-filter/controller.ts +++ b/src/property-filter/controller.ts @@ -14,7 +14,6 @@ import { InternalQuery, InternalToken, InternalTokenGroup, - isInternalToken, JoinOperation, ParsedText, Query, @@ -22,6 +21,7 @@ import { TokenGroup, } from './interfaces'; import { + isInternalToken, matchFilteringProperty, matchOperator, matchOperatorPrefix, diff --git a/src/property-filter/interfaces.ts b/src/property-filter/interfaces.ts index bf9b8cddb2..548af4bad1 100644 --- a/src/property-filter/interfaces.ts +++ b/src/property-filter/interfaces.ts @@ -416,19 +416,3 @@ export type ParsedText = | { step: 'property'; property: InternalFilteringProperty; operator: ComparisonOperator; value: string } | { step: 'operator'; property: InternalFilteringProperty; operatorPrefix: string } | { step: 'free-text'; operator?: ComparisonOperator; value: string }; - -// Type guards for InternalToken | InternalTokenGroup union discrimination. -// Using keyof ensures compile-time validation of the discriminant property name. -// See AWSUI-59006 for context. - -export function isInternalToken(tokenOrGroup: InternalToken | InternalTokenGroup): tokenOrGroup is InternalToken { - const key: keyof InternalToken = 'operator'; - return key in tokenOrGroup; -} - -export function isInternalTokenGroup( - tokenOrGroup: InternalToken | InternalTokenGroup -): tokenOrGroup is InternalTokenGroup { - const key: keyof InternalTokenGroup = 'operation'; - return key in tokenOrGroup; -} diff --git a/src/property-filter/token.tsx b/src/property-filter/token.tsx index 21a233278a..dc4363b9b6 100644 --- a/src/property-filter/token.tsx +++ b/src/property-filter/token.tsx @@ -19,13 +19,11 @@ import { InternalQuery, InternalToken, InternalTokenGroup, - isInternalToken, - isInternalTokenGroup, JoinOperation, LoadItemsDetail, } from './interfaces'; import { TokenEditor } from './token-editor'; -import { tokenGroupToTokens } from './utils'; +import { isInternalToken, isInternalTokenGroup, tokenGroupToTokens } from './utils'; import analyticsSelectors from './analytics-metadata/styles.css.js'; import styles from './styles.css.js'; diff --git a/src/property-filter/utils.ts b/src/property-filter/utils.ts index fbc31adac9..e018855c91 100644 --- a/src/property-filter/utils.ts +++ b/src/property-filter/utils.ts @@ -6,9 +6,22 @@ import { InternalFilteringOption, InternalFilteringProperty, InternalToken, + InternalTokenGroup, Token, } from './interfaces'; +export function isInternalToken(tokenOrGroup: InternalToken | InternalTokenGroup): tokenOrGroup is InternalToken { + const key: keyof InternalToken = 'operator'; + return key in tokenOrGroup; +} + +export function isInternalTokenGroup( + tokenOrGroup: InternalToken | InternalTokenGroup +): tokenOrGroup is InternalTokenGroup { + const key: keyof InternalTokenGroup = 'operation'; + return key in tokenOrGroup; +} + // Finds the longest property the filtering text starts from. export function matchFilteringProperty( filteringProperties: readonly InternalFilteringProperty[],