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:
parent
095d406779
commit
d3fb84b76d
1 changed files with 274 additions and 40 deletions
|
|
@ -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 ? '~' : '^'
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue