///
/**
* 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>;
}