ruipin / fvtt-lib-wrapper

Library for Foundry VTT which provides module developers with a simple way to modify core Foundry VTT code, while reducing the likelihood of conflict with other modules.
GNU Lesser General Public License v3.0
35 stars 15 forks source link

Add standardised patching framework #15

Open ruipin opened 3 years ago

ruipin commented 3 years ago

Inspired by some modules like Mess which patch methods using a regex, I have been wondering about standardising an API to do this officially supported by libWrapper, and that attempts to handle possible issues such as conflicts.

This API could be e.g.

libWrapper.patch("module-id", "path.to.method", /match/, "replace");

and for ease-of-use, also support

libWrapper.patch("module-id", "path.to.method",
  [/match1/, "replace1"],
  [/match2/, "replace2"],
  ...,
  [/matchN/, "replaceN"]
);

Each pair would behave exactly like a call to String.prototype.replace, so would also support replacement methods, etc.

libWrapper would then be responsible for working out which method to patch (including, if necessary, modifying a prior patch), and automatically setting up a corresponding OVERRIDE.

This would mean that two modules patching the same method, as long as their regexes don't overlap, would still be compatible.

Modules would be responsible for catching the exceptions if they require fall-back behaviour. As usual, uncaught exceptions will be raised to the user as errors.

The shim could be given a naive fallback implementation, that just does toString(), replace, and then overwrites the original.

manuelVo commented 3 years ago

Having done some patching in Drag Ruler I'd like to share some insights I gained through that process.

Here are my thoughts about the API:

Here are some pitfalls to be aware of when patching other functions:

ruipin commented 3 years ago

@manuelVo Thanks for your comments. They are extremely useful.

I think it should be possible for multiple modules to patch the same function [...]

Yeah, the proposal above would allow multiple modules to patch the same function as long as there's no overlap.

A way to insert after or before the regex (in addition to replacing) might be useful A way to add additional parameters to an existing function would be neat [...] As an alternative it would probably neat if a patch-file could be supplied instead of a list of replacements. [...]

Good points. I'm thinking to change the pairs to tuples, where the first entry is the type of operation.

We could have:

  1. replace/replaceAll: Calls the corresponding JS String.prototype functions
  2. prepend/prependAll: Inserts text before the first match / all matches
  3. append/appendAll: Inserts text after the first match / all matches
  4. diff: Applies a patch, using diff syntax. Probably would use a node module like diff

Due to the extra complexities, it's possible 4 would be added later, and not with the first implementation of this API.

The output of toString for a function is in no way normalized [...] Especially function signatures may need to be transformed [...]

I hadn't thought of the line-breaks, you're right this should be normalised. The function signature, I'm aware.

My plan was to extract the parameters and body and then recreate the signature myself. I would also clean up the string (e.g. remove comments, unify line breaks, deindent). The problem here is that this can't be done with regex, as there could be comments before the start of the body that contain { or (, or e.g. options = {} as a parameter. Using something like decomment might be feasible. Alternatively, a full-blown javascript parser might be the only choice here, e.g. esprima. More investigation is necessary.

An API would be provided for people to use to obtain the libWrapper string representation of a method, onto which they'd be applying their patches.

If a function uses symbols that are not available in the global scope the patched function will fail to execute [...]

This is a big problem, I think. You're completely right. I don't think there's any way around this, unfortunately - can't get a function's scope. Fortunately I don't think FVTT relies on scoped variables very often, but it's certainly something to document.

This is especially likely to happen if the function that should be patched has already been overwritten by another module.

Not a big issue with libWrapper, since either we're patching the wrong code (and the patch would fail anyway), or the function is only wrapped by libWrapper in which case we know every single wrapper, and what the original method is.

akrigline commented 2 years ago

I know this isn't a super high priority feature given its relatively limited uses, but I want to thank both of you for the documentation of the challenges here as they helped me in my quest to implement such a patching utility myself. I won't say I'm proud of my solution as it's very cobbled together, but it's something that would have been harder still to cobble together without your notes on the subject.