From 6e4840e1f3a7265b51759ace5d62a15f5e0adb47 Mon Sep 17 00:00:00 2001 From: KJ21-ENG <140263938+KJ21-ENG@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:28:11 +0000 Subject: [PATCH] Remove unsafe assertions from SearchRouter helpers Seatbelt-Operation: s08-a7f9ba43-4541881-sign-1 --- .../Search/SearchRouter/SearchRouterUtils.ts | 7 +- .../SearchRouter/buildSubstitutionsMap.ts | 7 +- ...getAutocompleteSelectionSubstitutionKey.ts | 4 +- .../SearchRouter/getQueryWithSubstitutions.ts | 8 +-- .../getUpdatedSubstitutionsMap.ts | 4 +- src/components/Search/types.ts | 14 ++++ src/libs/SearchParser/autocompleteParser.d.ts | 67 +++++++++++++++++++ 7 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 src/libs/SearchParser/autocompleteParser.d.ts diff --git a/src/components/Search/SearchRouter/SearchRouterUtils.ts b/src/components/Search/SearchRouter/SearchRouterUtils.ts index dc69694b7804..7475543f9ef6 100644 --- a/src/components/Search/SearchRouter/SearchRouterUtils.ts +++ b/src/components/Search/SearchRouter/SearchRouterUtils.ts @@ -2,8 +2,6 @@ import type {SearchQueryItem} from '@components/Search/SearchList/ListItem/Searc import {getPolicyNameWithFallback, sanitizeSearchValue} from '@libs/SearchQueryUtils'; -import type {ReportsSplitNavigatorParamList} from '@navigation/types'; - import CONST from '@src/CONST'; import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; @@ -50,8 +48,9 @@ function getContextualReportData(state: NavigationState | undefined): Contextual } if (maybeReportRoute?.name === SCREENS.REPORT || maybeReportRoute?.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT) { - // We're guaranteed that the type of params is of SCREENS.REPORT - return {contextualReportID: (maybeReportRoute?.params as ReportsSplitNavigatorParamList[typeof SCREENS.REPORT]).reportID, isSearchRouterScreen}; + const params = maybeReportRoute.params; + const reportID = params && 'reportID' in params ? params.reportID : undefined; + return {contextualReportID: typeof reportID === 'string' ? reportID : undefined, isSearchRouterScreen}; } return {contextualReportID: undefined, isSearchRouterScreen}; } diff --git a/src/components/Search/SearchRouter/buildSubstitutionsMap.ts b/src/components/Search/SearchRouter/buildSubstitutionsMap.ts index d125e7d6e61d..8b3dc49ab652 100644 --- a/src/components/Search/SearchRouter/buildSubstitutionsMap.ts +++ b/src/components/Search/SearchRouter/buildSubstitutionsMap.ts @@ -1,5 +1,4 @@ import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; -import type {SearchAutocompleteQueryRange} from '@components/Search/types'; import {parse} from '@libs/SearchParser/autocompleteParser'; import {getFilterDisplayValue} from '@libs/SearchQueryUtils'; @@ -45,7 +44,7 @@ function buildSubstitutionsMap( reportAttributes: ReportAttributesDerivedValue['reports'] | undefined, bankAccountList?: BankAccountList, ): SubstitutionMap { - const parsedQuery = parse(query) as {ranges: SearchAutocompleteQueryRange[]}; + const parsedQuery = parse(query); const searchAutocompleteQueryRanges = parsedQuery.ranges; if (searchAutocompleteQueryRanges.length === 0) { @@ -54,7 +53,7 @@ function buildSubstitutionsMap( const substitutionKeyOccurrences = new Map(); - const substitutionsMap = searchAutocompleteQueryRanges.reduce((map, range) => { + const substitutionsMap = searchAutocompleteQueryRanges.reduce((map, range) => { const {key: filterKey, value: filterValue} = range; if (filterKey === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE) { @@ -116,7 +115,7 @@ function buildSubstitutionsMap( } return map; - }, {} as SubstitutionMap); + }, {}); return substitutionsMap; } diff --git a/src/components/Search/SearchRouter/getAutocompleteSelectionSubstitutionKey.ts b/src/components/Search/SearchRouter/getAutocompleteSelectionSubstitutionKey.ts index eb0559866a72..553719ce8dcc 100644 --- a/src/components/Search/SearchRouter/getAutocompleteSelectionSubstitutionKey.ts +++ b/src/components/Search/SearchRouter/getAutocompleteSelectionSubstitutionKey.ts @@ -1,9 +1,7 @@ -import type {SearchAutocompleteResult} from '@components/Search/types'; - import {parse as parseSearchQuery} from '@libs/SearchParser/autocompleteParser'; function getAutocompleteSelectionSubstitutionKey(newSearchQuery: string, fieldKey: string, fallbackMapKey: string, fallbackSearchQuery: string): string { - const parsed = parseSearchQuery(newSearchQuery) as SearchAutocompleteResult; + const parsed = parseSearchQuery(newSearchQuery); const sameKeyRanges = parsed.ranges?.filter((range) => range.key === fieldKey) ?? []; const lastRange = sameKeyRanges.at(-1); const rangeValue = lastRange?.value ?? fallbackSearchQuery; diff --git a/src/components/Search/SearchRouter/getQueryWithSubstitutions.ts b/src/components/Search/SearchRouter/getQueryWithSubstitutions.ts index 99a4eb58df14..4a7bcfb19626 100644 --- a/src/components/Search/SearchRouter/getQueryWithSubstitutions.ts +++ b/src/components/Search/SearchRouter/getQueryWithSubstitutions.ts @@ -1,4 +1,4 @@ -import type {SearchAutocompleteQueryRange, SearchFilterKey} from '@components/Search/types'; +import type {SearchAutocompleteParserRange} from '@components/Search/types'; import {parse} from '@libs/SearchParser/autocompleteParser'; import {sanitizeSearchValue} from '@libs/SearchQueryUtils'; @@ -7,7 +7,7 @@ import CONST from '@src/CONST'; type SubstitutionMap = Record; -const getSubstitutionMapKey = (filterKey: SearchFilterKey, value: string) => `${filterKey}:${value}`; +const getSubstitutionMapKey = (filterKey: SearchAutocompleteParserRange['key'], value: string) => `${filterKey}:${value}`; const USER_FILTER_KEYS = new Set([ CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM, @@ -23,7 +23,7 @@ const USER_FILTER_KEYS = new Set([ * Key for the Nth occurrence of the same filter+value (e.g. multiple workspaces with the same name). * Index 0 uses the base key for backward compatibility; index > 0 uses baseKey:index. */ -const getSubstitutionMapKeyWithIndex = (filterKey: SearchFilterKey, value: string, index: number) => +const getSubstitutionMapKeyWithIndex = (filterKey: SearchAutocompleteParserRange['key'], value: string, index: number) => index === 0 ? getSubstitutionMapKey(filterKey, value) : `${getSubstitutionMapKey(filterKey, value)}:${index}`; /** @@ -41,7 +41,7 @@ const getSubstitutionMapKeyWithIndex = (filterKey: SearchFilterKey, value: strin * return: `A from:9876 A` */ function getQueryWithSubstitutions(changedQuery: string, substitutions: SubstitutionMap, currentUserAccountID?: number) { - const parsed = parse(changedQuery) as {ranges: SearchAutocompleteQueryRange[]}; + const parsed = parse(changedQuery); const searchAutocompleteQueryRanges = parsed.ranges; diff --git a/src/components/Search/SearchRouter/getUpdatedSubstitutionsMap.ts b/src/components/Search/SearchRouter/getUpdatedSubstitutionsMap.ts index 5d432786ee68..1f4b4c2ec104 100644 --- a/src/components/Search/SearchRouter/getUpdatedSubstitutionsMap.ts +++ b/src/components/Search/SearchRouter/getUpdatedSubstitutionsMap.ts @@ -1,5 +1,3 @@ -import type {SearchAutocompleteQueryRange} from '@components/Search/types'; - import {parse} from '@libs/SearchParser/autocompleteParser'; import type {SubstitutionMap} from './getQueryWithSubstitutions'; @@ -20,7 +18,7 @@ import {getSubstitutionMapKeyWithIndex} from './getQueryWithSubstitutions'; * return: {} */ function getUpdatedSubstitutionsMap(query: string, substitutions: SubstitutionMap): SubstitutionMap { - const parsedQuery = parse(query) as {ranges: SearchAutocompleteQueryRange[]}; + const parsedQuery = parse(query); const searchAutocompleteQueryRanges = parsedQuery.ranges; diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 0ad23ce0f847..7439411e34c8 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -411,6 +411,18 @@ type SearchAutocompleteQueryRange = { value: string; }; +/** Parser keys retain input spelling and can include dynamic report field suffixes. */ +type SearchAutocompleteParserRange = Omit & { + key: string; + negated: boolean; +}; + +type SearchAutocompleteParserResult = { + /** Whitespace trimmed from an identifier can leave only its key and negation. */ + autocomplete: SearchAutocompleteParserRange | Pick | null; + ranges: SearchAutocompleteParserRange[]; +}; + type SearchParams = { queryJSON: Readonly; searchKey: SearchKey | undefined; @@ -478,6 +490,8 @@ type SearchFilterCommonProps = { }; export type { + SearchAutocompleteParserRange, + SearchAutocompleteParserResult, SelectedTransactionInfo, SelectedTransactions, SearchColumnType, diff --git a/src/libs/SearchParser/autocompleteParser.d.ts b/src/libs/SearchParser/autocompleteParser.d.ts new file mode 100644 index 000000000000..53af06afffc6 --- /dev/null +++ b/src/libs/SearchParser/autocompleteParser.d.ts @@ -0,0 +1,67 @@ +/** + * Describes autocompleteParser.peggy and baseRules.peggy after parser-workletization.sh. + * Keep this contract aligned with the shipped autocompleteParser.js when regenerating it. + */ +import type {SearchAutocompleteParserResult} from '@components/Search/types'; + +type ParserExpectation = + | {type: 'literal'; text: string; ignoreCase: boolean} + | {type: 'class'; parts: Array; inverted: boolean; ignoreCase: boolean} + | {type: 'any'} + | {type: 'end'} + | {type: 'other'; description: string}; + +type ParserPosition = { + offset: number; + line: number; + column: number; +}; + +type ParserLocation = { + source: unknown; + start: ParserPosition; + end: ParserPosition; +}; + +type ParserOptions = { + startRule?: 'query' | ''; + grammarSource?: unknown; + peg$currPos?: number; + peg$silentFails?: number; + peg$maxFailExpected?: ParserExpectation[]; + peg$library?: boolean; +}; + +type ParserLibraryResult = { + peg$result: SearchAutocompleteParserResult; + peg$currPos: number; + peg$FAILED: Record; + peg$maxFailExpected: ParserExpectation[]; + peg$maxFailPos: number; +}; + +declare function parse(input: string, options?: ParserOptions & {peg$library?: false}): SearchAutocompleteParserResult; +declare function parse(input: string, options: ParserOptions & {peg$library: true}): ParserLibraryResult; +declare function parse(input: string, options?: ParserOptions): SearchAutocompleteParserResult | ParserLibraryResult; + +/** Initially contains query. Mutating this list does not register additional parser rules. */ +declare const StartRules: string[]; + +/** Workletization replaces the Error subclass with an empty constructor that ignores its arguments. */ +declare class SyntaxError { + constructor(message?: string, expected?: ParserExpectation[], found?: string | null, location?: ParserLocation); + + message?: string; + + expected?: ParserExpectation[]; + + found?: string | null; + + location?: ParserLocation; + + format(sources: Array<{source: unknown; text: string}>): string; + + static buildMessage(expected: ParserExpectation[], found: string | null): string; +} + +export {parse, StartRules, SyntaxError};