/// /** * Resolves the selector strings accepted by `Instance:QueryDescendants()` to the type of the * instances they match, so string-literal selectors get a precise return type. The grammar is * documented at https://create.roblox.com/docs/reference/engine/classes/Instance#QueryDescendants * * Only the class named in the last combinator segment affects the result, unioned across * comma-separated selectors. `.Tag` and `#Name` carry no class information and resolve to * `Instance`; attribute filters and pseudo-classes never change the class. Note that * whitespace is not a combinator in this grammar (descendant matching is `>>`), so spaces * are ignored rather than treated as separators. * * Dynamic templates and selectors exceeding the bounded analysis budget fall back to * `Instance`. Validation checks unsupported pseudo-classes and empty list entries; it * is not a complete runtime grammar check. */ declare namespace Selector { // Bounds are deliberately below TypeScript's recursion limit. A broad string means // analysis stopped; callers must preserve it as an Instance fallback, never a partial type. type OtherWhitespace = "\t" | "\n" | "\r" | "\f" | "\v"; type Trim = []> = Steps["length"] extends 128 ? string : T extends `${infer S} ` ? Trim : T extends ` ${infer S}` ? Trim : T; // Infinite template-literal key sets (including `${number}`) have no required keys. // Unlike `string extends S`, this also detects selectors such as `Part.${string}`. type IsDynamic = Record extends Record ? true : false; // Only the final combinator segment can determine the subject type. type LastSegment = []> = Steps["length"] extends 128 ? string : S extends `${string}>${infer R}` ? LastSegment : S; type CutAt = S extends `${infer Prefix}${D}${string}` ? Prefix : S; type LeadingClass = CutAt, ":">, ".">, "#">, " ">; // Consume a quoted value without interpreting the other quote character as syntax. // Values without backslashes use a single template match, regardless of their length. type QuotedRest = S extends `${infer A}${Quote}${infer B}` ? A extends `${string}\\${string}` ? EscapedQuotedRest : B : string; type EscapedQuotedRest< S extends string, Quote extends string, Steps extends Array = [], > = Steps["length"] extends 256 ? string : S extends `\\${string}${infer Rest}` ? EscapedQuotedRest : S extends `${Quote}${infer Rest}` ? Rest : S extends `${string}${infer Rest}` ? EscapedQuotedRest : string; type StripQuote< S extends string, Quote extends "'" | '"', Out extends string, Steps extends Array, > = S extends `${infer A}${Quote}${infer B}` ? QuotedRest extends infer Rest extends string ? string extends Rest ? string : StripQuotes : never : `${Out}${S}`; type StripQuotes< S extends string, Out extends string = "", Steps extends Array = [], > = Steps["length"] extends 128 ? string : S extends `${infer A}'${string}` ? A extends `${string}"${string}` ? StripQuote : StripQuote : S extends `${string}"${string}` ? StripQuote : `${Out}${S}`; // Quoted delimiters must be removed before filters or pseudo-class groups are read. // Sharing this normalization between validation and inference also lets the checker cache it. // A dot is an opaque filter marker: it preserves a nonempty selector inside :not([$x]). type StripFilters = []> = Steps["length"] extends 128 ? string : S extends `${infer A}[${string}]${infer B}` ? StripFilters<`${A}.${B}`, [...Steps, unknown]> : S; type ReplaceWhitespace< S extends string, W extends string, Steps extends Array = [], > = Steps["length"] extends 128 ? string : S extends `${infer A}${W}${infer B}` ? ReplaceWhitespace<`${A} ${B}`, W, [...Steps, unknown]> : S; type NormalizeWhitespace = S extends `${string}${OtherWhitespace}${string}` ? ReplaceWhitespace< ReplaceWhitespace, "\n">, "\r">, "\f">, "\v" > : S; type Normalize = NormalizeWhitespace>>; // Jump between parentheses rather than visiting every character. Quoted values and // filters are already opaque, so only these delimiters affect nesting. type StripClose< S extends string, Out extends string, Depth extends Array, Steps extends Array, > = S extends `${string})${infer Rest}` ? Depth extends [unknown, ...infer D] ? StripParens : string : string; type StripParens< S extends string, Out extends string = "", Depth extends Array = [], Steps extends Array = [], > = Steps["length"] extends 128 ? string : S extends `${infer A}(${infer B}` ? A extends `${string})${string}` ? StripClose : StripParens : S extends `${string})${string}` ? StripClose : Depth extends [] ? `${Out}${S}` : string; type FastClause = S extends keyof Instances ? Instances[S] : LeadingClass> extends infer C extends keyof Instances ? Instances[C] : Instance; // Accumulate the union so long lists remain tail-recursive. type FastSolve = []> = Steps["length"] extends 128 ? Instance : S extends `${infer A},${infer B}` ? FastSolve, [...Steps, unknown]> : Result | FastClause; type StripFlatParens = []> = Steps["length"] extends 128 ? string : S extends `${infer A}(${string})${infer B}` ? StripFlatParens<`${A}${B}`, [...Steps, unknown]> : S; type SolveNormalized = string extends S ? Instance : S extends `${string}(${string}` ? S extends `${string}(${string}(${string})${string})${string}` ? FastSolve> : FastSolve> : S extends `${string},${string}` ? FastSolve : FastClause; type SupportedPseudo = "not" | "has"; type CheckPseudos = []> = Steps["length"] extends 128 ? never : S extends `${string}:${infer R}` ? R extends `${infer Name}(${infer Rest}` ? Name extends SupportedPseudo ? CheckPseudos : Name : R // ':' not followed by 'name(' -> pseudo-classes require arguments : never; // Parentheses delimit nested lists just as commas delimit their entries. type HasEmptyGroup = []> = Steps["length"] extends 128 ? false : S extends `${string}(${infer Rest}` ? Trim extends `)${string}` | `,${string}` ? true : HasEmptyGroup : false; // The whole-empty selector "" is intentionally allowed. type HasEmptyListItem = S extends `${string},${string}` ? CheckListItems : false; type CheckListItems = []> = Steps["length"] extends 128 ? false : S extends `${infer A},${infer B}` ? Trim extends "" | `${string}(` | `)${string}` ? true : CheckListItems : Trim extends "" | `)${string}` ? true : false; type ValidateUnquoted = Q extends `${string}:${string}` ? CheckPseudos extends infer Bad ? [Bad] extends [never] ? true extends HasEmptyListItem | HasEmptyGroup ? `Invalid selector: empty selector in list (check for a stray or trailing comma)` : S : `Invalid selector: ':${Bad & string}' is not a supported pseudo-class (only ':not()' and ':has()' are allowed)` : never : Q extends `${string},${string}` ? true extends HasEmptyListItem | HasEmptyGroup ? `Invalid selector: empty selector in list (check for a stray or trailing comma)` : S : S; // Distribute over the original selector so each normalized branch retains its own input. export type ValidateSelector = S extends `${string}${":" | ","}${string}` ? IsDynamic extends true ? S : Normalize extends infer Q extends string ? string extends Q ? S : ValidateUnquoted : never : S; export type Solve = S extends keyof Instances ? Instances[S] : IsDynamic extends true ? Instance : SolveNormalized>; }