feat(lib/modules/finders): new withDependencies builders

- `skip` and `last`: Skips the first N/to the end of dependencies before doing comparisons.
- `atLeast` and `atMost`: Length check before doing comparisons.
- `includes`: Simply checks if the depedencies exist. This is performance intensive though.
This commit is contained in:
PalmDevs 2026-08-12 15:12:08 +07:00
parent 095d406779
commit d3fb84b76d
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA

View file

@ -18,8 +18,18 @@ export interface ComparableDependencyMap
| ComparableDependencyMap | ComparableDependencyMap
| Filter | Filter
> { > {
// loose
l?: boolean l?: boolean
// relative
r?: number r?: number
// skip
s?: number
// atLeast
n?: number
// atMost
x?: number
// includes
i?: boolean
} }
/** /**
@ -30,6 +40,9 @@ export interface ComparableDependencyMap
* To do proper fingerprinting for modules: * To do proper fingerprinting for modules:
* @see {@link withDependencies.loose} to loosen the checks. * @see {@link withDependencies.loose} to loosen the checks.
* @see {@link withDependencies.relative} to compare dependencies relatively. * @see {@link withDependencies.relative} to compare dependencies relatively.
* @see {@link withDependencies.skip} and {@link withDependencies.last} to move where comparison starts.
* @see {@link withDependencies.atLeast} and {@link withDependencies.atMost} to bound the dependency count.
* @see {@link withDependencies.includes} to compare without caring about order.
* *
* @example * @example
* ```ts * ```ts
@ -75,24 +88,26 @@ export interface ComparableDependencyMap
*/ */
const withDependencies_ = createFilterGenerator<Parameters<WithDependencies>>( const withDependencies_ = createFilterGenerator<Parameters<WithDependencies>>(
([deps], id) => depCompare(getModuleDependencies(id)!, deps, id, id), ([deps], id) => depCompare(getModuleDependencies(id)!, deps, id, id),
// The dep map is keyed as a nested set, so its modifiers are already part of the key
deps => `revenge.deps(${depGenFilterKey(deps)})`, deps => `revenge.deps(${depGenFilterKey(deps)})`,
FilterScopes.Uninitialized | FilterScopes.Initialized, FilterScopes.Uninitialized | FilterScopes.Initialized,
) as WithDependencies ) as WithDependencies
export const withDependencies = __DEV__ export const withDependencies = __DEV__
? (((deps: ComparableDependencyMap) => { ? (Object.assign((deps: ComparableDependencyMap) => {
// Warn about using undefined in deps, which is likely a mistake DEBUG_validateWithDependenciesFilter(deps)
for (let i = 0; i < deps.length; i++) {
if (deps[i] === undefined)
DEBUG_warnBadWithDependenciesFilter(deps, i)
}
return withDependencies_(deps) return withDependencies_(deps)
}) as WithDependencies) }, withDependencies_) as WithDependencies)
: withDependencies_ : withDependencies_
withDependencies.loose = loose withDependencies.loose = loose
withDependencies.relative = relative withDependencies.relative = relative
withDependencies.skip = skip
withDependencies.last = last
withDependencies.atLeast = atLeast
withDependencies.atMost = atMost
withDependencies.includes = includes
type WithDependencies = FilterGenerator< type WithDependencies = FilterGenerator<
<T>(deps: ComparableDependencyMap) => Filter<{ <T>(deps: ComparableDependencyMap) => Filter<{
@ -105,6 +120,11 @@ type WithDependencies = FilterGenerator<
> & { > & {
loose: typeof loose loose: typeof loose
relative: typeof relative relative: typeof relative
skip: typeof skip
last: typeof last
atLeast: typeof atLeast
atMost: typeof atMost
includes: typeof includes
} }
/** /**
@ -121,6 +141,96 @@ function loose(deps: ComparableDependencyMap) {
return deps return deps
} }
/**
* Skip a number of dependencies before comparing positionally.
*
* Passing `Infinity` anchors the set to the end, matching the **last** `deps.length` dependencies.
* Anything before them is unconstrained.
*
* @param amount The amount of dependencies to skip from the start, or `Infinity` to anchor to the end.
* @param deps The dependency map to skip in. This permanently modifies the array.
* @returns The modified dependency map.
*
* @see {@link withDependencies.last} for the `Infinity` shorthand.
*/
function skip(amount: number, deps: ComparableDependencyMap = []) {
deps.s = amount
return deps
}
/**
* Match the **last** `deps.length` dependencies, leaving anything before them unconstrained.
*
* Shorthand for {@link withDependencies.skip} with `Infinity`.
* Prefer this over leading comparisons when a module's trailing dependencies are the stable part of its fingerprint.
*
* @param deps The dependency map to anchor to the end. This permanently modifies the array.
* @returns The modified dependency map.
*
* @example
* ```ts
* const { last, relative } = withDependencies
*
* // Matches modules whose last three dependencies are [Any, module ID + 1, 2]
* withDependencies(last([null, relative(1), 2]))
* ```
*/
function last(deps: ComparableDependencyMap = []) {
deps.s = Infinity
return deps
}
/**
* Require the module to have at least `count` dependencies.
*
* This implies {@link withDependencies.loose}, as an exact length check would never pass alongside a bound.
*
* @param count The minimum amount of dependencies.
* @param deps The dependency map to bound. This permanently modifies the array.
* @returns The modified dependency map.
*/
function atLeast(count: number, deps: ComparableDependencyMap = []) {
deps.n = count
deps.l = true
return deps
}
/**
* Require the module to have at most `count` dependencies.
*
* This implies {@link withDependencies.loose}, as an exact length check would never pass alongside a bound.
*
* @param count The maximum amount of dependencies.
* @param deps The dependency map to bound. This permanently modifies the array.
* @returns The modified dependency map.
*/
function atMost(count: number, deps: ComparableDependencyMap = []) {
deps.x = count
deps.l = true
return deps
}
/**
* Compare the set without caring about order or position, only that every dependency exists somewhere.
*
* Entries are matched independently, so two identical entries can both match the same dependency.
* Dynamic (`null`) entries are meaningless here and are ignored.
*
* **This is much more expensive than positional comparison**, as every entry is compared against every dependency.
* Bound it with {@link withDependencies.atLeast} or {@link withDependencies.atMost} where possible, as those are checked first.
*
* @param deps The dependency map to compare unordered. This permanently modifies the array.
* @returns The modified dependency map.
*/
function includes(deps: ComparableDependencyMap) {
if (__DEV__)
for (let i = 0; i < deps.length; i++)
if (deps[i] == null) DEBUG_warnBadIncludesDependency(deps, i)
deps.i = true
return deps
}
const RelativeSignBit = 1 << 30 const RelativeSignBit = 1 << 30
const RelativeBit = 1 << 29 const RelativeBit = 1 << 29
const RelativeRootBit = 1 << 28 const RelativeRootBit = 1 << 28
@ -190,6 +300,70 @@ function DEBUG_warnBadWithDependenciesFilter(
) )
} }
/**
* Warns the developer about a dynamic dependency in a `withDependencies.includes` set, which matches anything and is therefore a no-op.
*/
function DEBUG_warnBadIncludesDependency(
deps: ComparableDependencyMap,
index: number,
) {
nativeLoggingHook(
`\u001b[33mBad withDependencies.includes set, dynamic ID at index ${index} matches anything: [${depGenFilterKey(deps)}]\n${getCurrentStack()}\u001b[0m`,
2,
)
}
/**
* Warns the developer about dependency count bounds that contradict, or are already implied by, the set being compared.
*/
function DEBUG_warnBadDependencyBounds(
deps: ComparableDependencyMap,
reason: string,
) {
nativeLoggingHook(
`\u001b[33mBad withDependencies filter, ${reason}: [${depGenFilterKey(deps)}]\n${getCurrentStack()}\u001b[0m`,
2,
)
}
/**
* Validates a dependency map and its nested sets, warning about mistakes that would otherwise silently never match.
*/
function DEBUG_validateWithDependenciesFilter(deps: ComparableDependencyMap) {
const { n: min, x: max, s: skip } = deps
// The lowest dependency count the set itself can match.
// Unordered sets have no window, so they only ever constrain the count through bounds.
const floor = deps.i
? 0
: (skip === undefined || skip === Infinity ? 0 : skip) + deps.length
if (max !== undefined && max < floor)
DEBUG_warnBadDependencyBounds(
deps,
`atMost(${max}) can never match, the set requires at least ${floor} dependencies`,
)
else if (min !== undefined && max !== undefined && min > max)
DEBUG_warnBadDependencyBounds(
deps,
`atLeast(${min}) can never match, atMost(${max}) is lower`,
)
else if (min !== undefined && min <= floor)
DEBUG_warnBadDependencyBounds(
deps,
`atLeast(${min}) does nothing, the set already requires at least ${floor} dependencies`,
)
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
// Warn about using undefined in deps, which is likely a mistake
if (dep === undefined) DEBUG_warnBadWithDependenciesFilter(deps, i)
else if (typeof dep === 'object' && dep !== null)
DEBUG_validateWithDependenciesFilter(dep)
}
}
function depCompare( function depCompare(
a: Metro.ModuleID[], a: Metro.ModuleID[],
b: ComparableDependencyMap, b: ComparableDependencyMap,
@ -198,7 +372,29 @@ function depCompare(
): boolean { ): boolean {
const lenA = a.length const lenA = a.length
const lenB = b.length const lenB = b.length
if (b.l ? lenA < lenB : lenA !== lenB) return false
const min = b.n
if (min !== undefined && lenA < min) return false
const max = b.x
if (max !== undefined && lenA > max) return false
if (b.i) return depIncludesCompare(a, b, root, parent)
// Index in a where positional comparison starts
let start = 0
const skip = b.s
if (skip === undefined) {
if (b.l ? lenA < lenB : lenA !== lenB) return false
} else if (skip === Infinity) {
// Anchor to the end, so everything before the last lenB dependencies is unconstrained
start = lenA - lenB
if (start < 0) return false
} else {
start = skip
if (b.l ? lenA < start + lenB : lenA !== start + lenB) return false
}
for (let i = 0; i < lenB; i++) { for (let i = 0; i < lenB; i++) {
const compare = b[i] const compare = b[i]
@ -209,43 +405,71 @@ function depCompare(
// Skip dynamic // Skip dynamic
if (compare == null) continue if (compare == null) continue
const id = a[i] const id = a[start + i]
// Very rare case where a module dependency has an unused `null` dependency // Very rare case where a module dependency has an unused `null` dependency
// TODO: Probably a Metro/Discord bug? // TODO: Probably a Metro/Discord bug?
if (id === null) return false if (id === null) return false
switch (typeof compare) { if (!depMatches(compare, id, root, parent)) return false
case 'function': {
const filter = compare
if (!runFilter(filter, id, getInitializedModuleExports(id)))
return false
continue
}
case 'object': {
const nested = compare as ComparableDependencyMap
// relative.withDependencies?
if (nested.r && !depShallowCompare(nested.r, id, root, parent))
return false
if (depCompare(getModuleDependencies(id)!, nested, root, id))
continue
return false
}
default: {
if (depShallowCompare(compare as number, id, root, parent))
continue
return false
}
}
} }
return true return true
} }
function depIncludesCompare(
a: Metro.ModuleID[],
b: ComparableDependencyMap,
root: Metro.ModuleID,
parent: Metro.ModuleID,
): boolean {
const lenA = a.length
for (let i = 0; i < b.length; i++) {
const compare = b[i]
// Dynamic dependencies match anything, so they carry no meaning when unordered
if (compare == null) continue
let found = false
for (let j = 0; j < lenA; j++) {
const id = a[j]
if (id !== null && depMatches(compare, id, root, parent)) {
found = true
break
}
}
if (!found) return false
}
return true
}
function depMatches(
compare: NonNullable<ComparableDependencyMap[number]>,
id: Metro.ModuleID,
root: Metro.ModuleID,
parent: Metro.ModuleID,
): boolean {
switch (typeof compare) {
case 'function':
return !!runFilter(compare, id, getInitializedModuleExports(id))
case 'object': {
const nested = compare
// relative.withDependencies?
if (nested.r && !depShallowCompare(nested.r, id, root, parent))
return false
return depCompare(getModuleDependencies(id)!, nested, root, id)
}
default:
return depShallowCompare(compare, id, root, parent)
}
}
function depShallowCompare( function depShallowCompare(
compare: number, compare: number,
id: Metro.ModuleID, id: Metro.ModuleID,
@ -289,11 +513,7 @@ function depGenFilterKey(deps: ComparableDependencyMap): string {
case 'object': { case 'object': {
// It's a nested dependency array // It's a nested dependency array
const nested = dep as ComparableDependencyMap const nested = dep as ComparableDependencyMap
if (nested.l) key += '#' key += `${depGenModifierKey(nested)}[${depGenFilterKey(nested)}],`
// relative.withDependencies?
if (nested.r) key += `${depGenRelativeKeyPart(nested.r)}:`
key += `[${depGenFilterKey(nested)}],`
break break
} }
default: { default: {
@ -309,6 +529,20 @@ function depGenFilterKey(deps: ComparableDependencyMap): string {
return key.substring(0, key.length - 1) return key.substring(0, key.length - 1)
} }
function depGenModifierKey(deps: ComparableDependencyMap): string {
let key = ''
if (deps.i) key += '?'
if (deps.l) key += '#'
if (deps.n !== undefined) key += `>${deps.n}`
if (deps.x !== undefined) key += `<${deps.x}`
if (deps.s !== undefined) key += `+${deps.s === Infinity ? '*' : deps.s}`
// relative.withDependencies?
if (deps.r) key += `${depGenRelativeKeyPart(deps.r)}:`
return key
}
function depGenRelativeKeyPart(dep: number) { function depGenRelativeKeyPart(dep: number) {
const magnitude = depGetRelMagnitude(dep) const magnitude = depGetRelMagnitude(dep)
const prefix = dep & RelativeRootBit ? '~' : '^' const prefix = dep & RelativeRootBit ? '~' : '^'