Diffsitter: A tree-sitter based AST difftool to get meaningful semantic diffs
github.com
github.com
Right now I have plans to implement the Myers diff algorithm to make diffsitter more efficient (profiling shows that the current biggest bottleneck is memory allocations), and after that I wanted to start experimenting with different heuristics to make the diffs themselves more useful. Support for the unified diff format is also in my plans.
It's nice to see people discussing the project, I'm very flattered that you guys have taken the time to look at it, and please feel free to open issues with features that you want to see or any shortcomings you've found.
The backstory for this project is that I once had to review a diff in a golang codebase that realigned a bit struct because someone added a new field, and I was slightly annoyed that I was seeing all these extra lines changed even though I really only cared about the fields that were added, so I did a little weekend project to see if it was possible to do a diff directly on the AST instead, and I've been working on this in small pieces ever since.
have you thought about using the ast representation to regenerate "canonicalized" versions of the source files upon which you can then represent the diffs (and context)?
(i'm not suggesting this be committed, but rather it be a view)
1. "let x = 1" was removed
2. "fn add_one {" was removed
3. a closing brace was added
4. "fn addition() {" was added
5. "fn add_two() {" was added
(1) is neat, (2) is reasonable. (3) showing up before both (4) and (5) is super weird. Is there a bug, or does this just demonstrate a disconnect between how a parser parses the code and how a human parses it?
It does demonstrate that diffsitter can tell that `fn main() {` was not semantically changed at all, while still being able to cope with syntactically invalid code like `fn add_one {`.
But look at the following, where I've attempted to manually convert diffsitter's generated diff into a unified diff format, making it easier to understand what's going on:
1 fn main() {
2 - let x = 1;
3 - fn add_one {
4 + }
5 + fn addition() {
6 }
7 + fn add_two() {
8 }
Look at the closing brace on line 6, which doesn't have a - or + in front of it, meaning that the diff tool has found a closing brace in the old code and one in the new code and decided that they correspond to each other. But this correspondence is not syntactically meaningful. In the old code, that closing brace belongs to `fn main`. In the new code, however, it belongs to `fn addition`, while `fn main`'s closing brace is now represented by the "inserted" closing brace on line 4!Mixing up braces this way is a typical weakness of traditional diff tools. I'd hope that a syntax-aware diff tool could prevent such mix-ups, and instead generate a diff like:
1 fn main() {
2 - let x = 1;
3 }
4 - fn add_one {
5 - }
6 + fn addition() {
7 + }
8 + fn add_two() {
9 + }
Even though this diff is one line longer (while diff tools typically aim to produce the shortest possible diff), it's more semantically correct, and would be much easier to deal with if a merge conflict came into play.Unfortunately, diffsitter does not do this, at least not in that demo. And I don't see how it would be [edit: caused by] a disconnect between how a parser parses the code and how a human parses it. The parser has the same idea of closing braces belonging to particular opening constructs as humans do.
I don't understand the argument you made here. Why couldn't it do this?
Compared to the first snippet, in the second snippet:
1. There is still a function called `main`, but it has no lines, compared to a single line before. The conclusion is that this line was removed.
2. There is no longer an `add_one` function.
3. There are two new functions, `addition` and `add_two`.
This is exactly what your wanted diff is showing and all of those things can be determined by a parser.
I'm redoing the diffing algo anyways, so I'll keep this in mind for the next go.
Edit: it's because I decided to split line-by-line a while back because I thought it would make the diffs easier to read (rather than squashing all of the tokens together, which is not very aesthetically pleasing), but also gives us unintuitive results like this.
Since we took git diffs as a target for semantic analysis, we have a different approach to our diffs. We start with line-by-line diffs (specifically using "git diff") and then take a semantic diff by superimposing the git diff information on top of the initial and terminal ASTs.
This makes the diff calculation cheaper because we don't have to do full diff between trees.
Haven't updated the code in a few months, but my team is actively using Locust on public GitHub repos to learn the semantics of those code bases. We do plan to do some work on it soon to make it easier to make Locust easier to use (especially as a library).
Really need to sit down and take a proper look at tree-sitter. We currently support Locust diffs for Python, Javascript, and Java, but each one is custom written and implements the same basic algorithm. It looks like tree sitter might just crush this problem for us.
It is especially exciting that they aim to provide useful information even in the presence of syntax errors.
To be fair, Treesitter is also very hard to use/implement into an editor. It however seem more portable as it can compile to wasm, if you could only figure out how to use it...
I think the problems with Treesitter could be solved with documentation and examples. I don't think the problems with the language server protocol can be fixed as there is too much work to make a full featured language server, so no one will be willing to maintain them.
While the language server turns the editor into a simple io-terminal, Treesitter is like: - here's a tree - you parse it. I would like there to be a middle ground, where you could for example request a list of all variables in scope in the source code on row X. Something ontop of Treesitter, or maybe Treesitter "plugins".
A language client is certainly difficult but only has to be created once per editor, and it might be a good thing that the lsp spec is so broad since server authors can implement features at their own pace, while users can have a core subset of features from the start.
I haven't seen any issues with maintenance with any servers I use, though I have seen development on some servers stop when there are better are newer alternatives for a specific language.
I like that LSP has become a multi-editor standard, but I wish language server developers would test their servers against at least a few other editors, or with users of other editors reporting bugs.
I'm adding something from my open tabs: https://fitzgeraldnick.com/2020/08/24/writing-a-test-case-ge...
Also, one nice thing about diff is that it also gives you a patch that will turn the input into the output. Can this do that?
Which is supposed to be difficult, if not impossible: https://www.perlmonks.org/?node_id=663393
I suppose, though, if it's for diffs and syntax highlighting, flaws matter less.
https://www.semanticmerge.com/
Would be cool if diffsitter added support for facilitating three way merges
The method used by diffsitter is to compute the path with minimal editing distance: https://github.com/afnanenayet/diffsitter/blob/main/src/ast.....
In the case where the edits are known (because you're re-creating the AST per keystroke), you can save parsing time by passing in the old tree and a set of edits: https://tree-sitter.github.io/tree-sitter/using-parsers#edit....
Intuitively, I feel like there should be a way to reduce diffing time as well to avoid O(mn) run and space time that comes from the dynamic programming solution to compute the minimal edit.
Also these resources:
My impression (from the outside looking in) is that there weren't many people making use of those capabilities, so it wasn't a high priority feature. A bit of a shame, though -- it's a really cool tool and I enjoyed building some things on top of it. I switched to a different front-end, though, when I saw that they had removed those features and all mention of incrementality/differencing from the documentations.
I'd be happy to be wrong about this, if anyone has more up-to-date information about semantic!
https://github.com/cregit/cregit https://lwn.net/Articles/698425/
Eg. with node project this is a trivial task with tools like Husky in place