This is an archive of the discontinued LLVM Phabricator instance.

[clangd] Implement cross-file rename.
ClosedPublic

Authored by hokein on Oct 21 2019, 8:13 AM.

Details

Summary

This is the initial version. The cross-file rename is purely based on
the index (plus a text-match verification).

It is hidden under a command-line flag, and only available for a small set
of symbols.

Diff Detail

Event Timeline

There are a very large number of changes, so older changes are hidden. Show Older Changes
Herald added a project: Restricted Project. · View Herald TranscriptOct 21 2019, 8:13 AM

tests are missing, but the patch should be sufficient for initial review.

Build result: fail - 33608 tests passed, 1 failed and 462 were skipped.

failed: LLVM.tools/llvm-ar/mri-utf8.test

Log files: cmake-log.txt, ninja_check_all-log.txt, CMakeCache.txt

Thanks for implementing this.
I think we could split it into multiple changes to make understanding it easier, see inline comments, I've tried to point out the places I find relevant.

Would definitely be nice to have some unit-tests for this.

Another important concern is surfacing errors to the users: silently dropping ranges for stale files is definitely not the nicest option, I'm afraid this will lead to non-explainable failure modes and users will be incredibly unhappy...

clang-tools-extra/clangd/ClangdLSPServer.cpp
106

NIT: rename to FE

clang-tools-extra/clangd/ClangdServer.h
334

NIT: this comment is redundant

clang-tools-extra/clangd/index/SymbolCollector.cpp
268 ↗(On Diff #225889)

Not sure what was filtered out here.
I suggest moving this to a separate change, with unit-tests clearly describing the new symbols we do not filter out anymore and an explanation why it's necessary.

WDYT?

clang-tools-extra/clangd/refactor/Rename.cpp
182

So llvm::None means we do not reject?
Probably worth documenting that.

Maybe even add an enumerator ReasonToReject::Accept, indicating the symbol should be accepted and get rid of Optional altogether.

Otherwise this leads to code like:

auto R = renameableOutsideFile(N, I);
if (!R) { // negative condition.
 return doRename(N, I); // But we're running the rename anyway... WAT?
}
``
184

This function resembles renamableWithinFile.
We should probably share implementation and pass an extra flag. Something like isRenamable(..., bool IsCrossFile).

WDYT?

215

NIT: else is redundant

363

I feel this code is fundamental to the trade-offs we made for rename.
Could we move this to a separate function and add unit-tests for it?

You probably want to have something that handles just a single file rather than all edits in all files, to avoid mocking VFS in tests, etc.

426

Can we rely on the fact that dynamic index should not be stale for the current file instead?
Or don't we have this invariant?

We end up having two implementations now: index-based and AST-based. It'd be nice to have just one.
If that's not possible in the first revision, could you explain why?

Note that moving the current-file rename to index-based implementation is independent of doing cross-file renames. Maybe we should start with it?

clang-tools-extra/clangd/refactor/Rename.h
33

Have we considered post-filtering instead?
Or are we concerned with performance implications of openning too many files to compute the corrected edit ranges?

Probably worth documenting why we need it.

33

NIT: the comment is redundant

clang-tools-extra/clangd/tool/ClangdMain.cpp
270

s/fature/feature

271

Indicating the default is probably redundant, LLVM options-parsing library will indicate the default on command-line anyway.

clang/lib/Tooling/Refactoring/Rename/USRFindingAction.cpp
44 ↗(On Diff #225889)

Could you move this to a separate change and add corresponding unit-tests?

hokein marked an inline comment as done.Oct 22 2019, 8:13 AM

Thanks for the comments.

Another important concern is surfacing errors to the users: silently dropping ranges for stale files is definitely not the nicest option, I'm afraid this will lead to non-explainable failure modes and users will be incredibly unhappy...

Agree, I think we should surface this error to users when the index is stale or we don't have enough confident to perform the rename.

Thinking more about this -- we have a dynamic index (for all opened files) which is overlaid on a static index (which is a background index in open-source world), so for all affected files, they are either in

  1. an open state -- we can rely on the dynamic index, I think it is safe to assume that index always returns up-to-date results;
  2. a non-open state -- rely on the background index, however background index has more chance to be stale (especially we don't detect file-change events at the moment), we could do a range patch heuristically to mitigate this stale issue. Failing that, we surface the error to users.
ilya-biryukov added a comment.EditedOct 23 2019, 2:16 AM

Thinking more about this -- we have a dynamic index (for all opened files) which is overlaid on a static index (which is a background index in open-source world), so for all affected files, they are either in

  1. an open state -- we can rely on the dynamic index, I think it is safe to assume that index always returns up-to-date results;

Not sure that holds. What if the file in question is being rebuilt right now? We do not wait until all ASTs are built (and the dynamic index gets the new results).
Open files actually pose an interesting problem: their contents do not correspond to the file contents on disk.
We should choose which of those we update on rename: dirty buffers or file contents? (I believe we should do dirty buffers, but I believe @sammccall has the opposite opinion, so we should sync on this)

  1. a non-open state -- rely on the background index, however background index has more chance to be stale (especially we don't detect file-change events at the moment), we could do a range patch heuristically to mitigate this stale issue. Failing that, we surface the error to users.

To summarize, I think files can be stale in both cases and we should patch the ranges in both cases.

hokein updated this revision to Diff 226137.Oct 23 2019, 7:41 AM
hokein marked 13 inline comments as done.
  • address comments;
  • add more FIXMEs;

Thinking more about this -- we have a dynamic index (for all opened files) which is overlaid on a static index (which is a background index in open-source world), so for all affected files, they are either in

  1. an open state -- we can rely on the dynamic index, I think it is safe to assume that index always returns up-to-date results;

Not sure that holds. What if the file in question is being rebuilt right now? We do not wait until all ASTs are built (and the dynamic index gets the new results).

I'm not sure this would happen quite often in practice (probably depends on users' behavior). but yes, patching ranges would mitigate this issue.

Open files actually pose an interesting problem: their contents do not correspond to the file contents on disk.
We should choose which of those we update on rename: dirty buffers or file contents? (I believe we should do dirty buffers, but I believe @sammccall has the opposite opinion, so we should sync on this)

I have the same feeling of using dirty buffers for open files, let's discuss offline.

  1. a non-open state -- rely on the background index, however background index has more chance to be stale (especially we don't detect file-change events at the moment), we could do a range patch heuristically to mitigate this stale issue. Failing that, we surface the error to users.

To summarize, I think files can be stale in both cases and we should patch the ranges in both cases.

clang-tools-extra/clangd/index/SymbolCollector.cpp
268 ↗(On Diff #225889)

Agreed. split it out.

clang-tools-extra/clangd/refactor/Rename.cpp
182

yeah, returning None means that we accept the rename.

Adding Accept is not aligning with the mental model, Accept doesn't belong to ReasonToReject I think. I agree the above given code is misleading, but looking at the current code in this file, it doesn't seem too bad, I think?

if (auto Reject = renameableOutsideFile(*RenameDecl, RInputs.Index))
   return makeError(*Reject);
184

though they look similar, the logic is quite different, for example, we query the index in within-file rename to detect a symbol is used outside of the file which is not needed for cross-file rename, and cross-file rename allows fewer symbols (as our index doesn't support them), so I don't think we can share a lot of implementation.

363

Agree, especially when we start implementing complicated stuff, e.g. range patching heuristics.

Moved it out, but note that I don't plan to add more stuff in this initial patch, so the logic is pretty straightforward, just assembling rename replacement from occurrences.

426

I think we can't get rid of the AST-based rename -- mainly for renaming local symbols (e.g. local variable in function body), as we don't index these symbols...

clang-tools-extra/clangd/refactor/Rename.h
33

Performance is one of the concern. I think the main purpose of this flag is to avoid any regressions in our existing within-file rename while developing the new cross-file rename.

Build result: fail - 59475 tests passed, 2 failed and 805 were skipped.

failed: Clangd.Clangd/rename.test
failed: LLVM.tools/llvm-ar/mri-utf8.test

Log files: cmake-log.txt, ninja_check_all-log.txt, CMakeCache.txt

Build result: fail - 59518 tests passed, 1 failed and 763 were skipped.

failed: Clangd.Clangd/rename.test

Log files: cmake-log.txt, ninja_check_all-log.txt, CMakeCache.txt

ilya-biryukov added inline comments.Oct 24 2019, 1:31 AM
clang-tools-extra/clangd/refactor/Rename.cpp
182

Agreed this aligns with the other functions we have in the file, so let's keep as is.

184

Can this be handled with a special flag:

bool renameable(NamedDecl &Decl, const SymbolIndex *Index, bool CrossFile) {
  if (CrossFileRename) {
    // check something special...
  }
}

Sharing implementation in the same function makes the differences between cross-file and single-file case obvious and also ensures any things that need to be updated for both are shared.

363

but note that I don't plan to add more stuff in this initial patch

Should we also check whether the replaced text matches the expected identifier and add unit-tests for this?
Or would you prefer to move all changes to this function to a separate patch?

403

Can we avoid duplicating calls to renamebleWithinFile? It should be possible to only call renameOutsideFile when CrossFileRename is enabled and always call renameWithinFile. Or is there something I'm missing?

426

Thanks, I believe you're right... We can't unify these implementations, at least not in the short term.

So renameOutsideFile fails on local symbols and renameWithinFile should handle that, right?
Also worth noting that renameWithinFile is AST-based and renameOutsideFile is index-based.
Could we document that?

hokein updated this revision to Diff 226414.Oct 25 2019, 5:11 AM
hokein marked 5 inline comments as done.
  • simplify the code, addressing comments;
  • add unittests;

Not sure that holds. What if the file in question is being rebuilt right now? We do not wait until all ASTs are built (and the dynamic index gets the new results).
Open files actually pose an interesting problem: their contents do not correspond to the file contents on disk.
We should choose which of those we update on rename: dirty buffers or file contents? (I believe we should do dirty buffers, but I believe @sammccall has the opposite opinion, so we should sync on this)

Based on the offline discussion, we decide to use dirty buffers for opened files.

clang-tools-extra/clangd/refactor/Rename.cpp
184

Done. I tried my best to unify them, please take a look on the new code.

363

I prefer to do it afterwards as the patch is relatively big now.

403

I was attempting to keep two branches (within-file rename, cross-file rename) as separately as possible, but I agree with your suggestion, it does simplify the code.

426

yes, exactly. I have simplified the code in this function, and added documentations.

Build result: fail - 59520 tests passed, 1 failed and 763 were skipped.

failed: Clangd.Clangd/rename.test

Log files: cmake-log.txt, ninja_check_all-log.txt, CMakeCache.txt

ilya-biryukov added inline comments.Oct 25 2019, 7:44 AM
clang-tools-extra/clangd/tool/ClangdMain.cpp
270

Could you document that the feature is highly experimental and may lead to broken code or incomplete renames for the time being?

Also, maybe make it hidden for now?
At least until we have basic validation of ranges from the index. In the current state we can easily produce edits to unrelated parts of the file...

clang-tools-extra/clangd/unittests/RenameTests.cpp
562

Thinking whether we can encode these transformations in a nicer way?
If we could convert the contents dirty buffer and replacements to something like:

class [[Foo |-> newName]] {};

Just a proposal, maybe there's a better syntax here.

We can easily match strings instead of matching ranges in the tests. This has the advantage of having textual diffs in case something goes wrong - much more pleasant than looking at the offsets in the ranges.

WDYT?

587

Instead of defining custom matchers, could we convert to standard types and use existing gtest matchers?
Something like std::vector<std::pair</*Path*/std::string, Edit>> should work just fine.

Huge advantage we'll get is better output in case of errors.

hokein updated this revision to Diff 226641.Oct 28 2019, 5:01 AM
hokein marked 3 inline comments as done.

address comments:

  • make the command-line flag hidden;
  • use the gtesting matcher;
clang-tools-extra/clangd/tool/ClangdMain.cpp
270

Done. yes, we should make it hidden.

clang-tools-extra/clangd/unittests/RenameTests.cpp
562

I agree textual diff would give a more friendly results when there are failures in the tests.

I don't have a better way to encode the transformation in the annotation code, I think a better way maybe to use a hard-coded new name, and applying the actual replacements on the testing code, and verify the the text diffs.

If we want to do this change, I'd prefer to do it in a followup, since it would change the existing testcases as well. What do you think?

587

yeah, converting the llvm::StringMap to standard types could work, but we have to pay the cost of transformation (that was something I tried to avoid).
I think there is no strong reason to use llvm::StringMap as the FileEdits, what do you think if we change the FileEdits type to std::vector<pair<string, Edit>>?

Build result: pass - 59701 tests passed, 0 failed and 763 were skipped.
Log files: cmake-log.txt, ninja_check_all-log.txt, CMakeCache.txt

Thanks, mostly LG!

The only important comment I have left is about limiting the number of references. Others are NITs.

clang-tools-extra/clangd/refactor/Rename.cpp
281

Why do we need to limit the number of references in the first place?

285

Maybe remove this comment?

286

Why not extract this as a helper function?

295

NIT: maybe put this comment on the line before the declaration?
Should lead to better formatting

Also, this comment would be most useful on the function declaration. Maybe move it there?

336

NIT: Maybe call it renameWithIndex instead?
It should capture what this function is doing better...

But up to you

351

Maybe move this comment to the function itself?
It definitely does a great job of capturing what this function is doing and what the trade-offs are.

clang-tools-extra/clangd/unittests/RenameTests.cpp
562

Maybe I'm overthinking it... For rename having just the range is probably enough.

clang-tools-extra/clangd/unittests/SyncAPI.cpp
106

s/DirtyBuffaer/DirtyBuffer

also use /*DirtryBuffer=*/ to be consistent with /*WantFormat=*/ from the previous line

hokein updated this revision to Diff 227278.Oct 31 2019, 7:07 AM
hokein marked 8 inline comments as done.

address comments.

hokein added inline comments.Oct 31 2019, 7:08 AM
clang-tools-extra/clangd/refactor/Rename.cpp
281

I was thinking this is for performance, if the index returns too many results (the number of affected files >> our limit), we will ends up doing many unnecessary work (like resolving URIs). Removed it now, it seems a premature optimization, we can revisit this afterwards.

336

I would keep the current name, as it is symmetrical to the renameWithinFile.

Build result: pass - 59816 tests passed, 0 failed and 762 were skipped.
Log files: console-log.txt, CMakeCache.txt

ilya-biryukov marked an inline comment as done.Oct 31 2019, 10:15 AM
ilya-biryukov added inline comments.
clang-tools-extra/clangd/refactor/Rename.cpp
282

NIT: grouped by the absolute file name? Or is there a better term than "absolute" to describe this?

336

Yeah, those go together. Using renameWithIndex would mean the other one should be renameWithAST (or similar).
Feel free to keep as is too.

clang-tools-extra/clangd/refactor/Rename.h
23

Could you document what does it mean for this function to return llvm::None?
I assume we read the contents from disk instead.

Also worth documenting what should be returned for MainFilePath? llvm::None? same value as MainFileCode?

24

NIT: maybe use function_ref? We definitely don't store this function anywhere.

hokein updated this revision to Diff 227663.Nov 4 2019, 1:58 AM
hokein marked 4 inline comments as done.

Add comments for DirtyBufferGetter.

Build result: pass - 59816 tests passed, 0 failed and 762 were skipped.
Log files: console-log.txt, CMakeCache.txt

ilya-biryukov added inline comments.Nov 4 2019, 3:00 AM
clang-tools-extra/clangd/ClangdLSPServer.cpp
766

NIT: is capture of this redundant here?

I could be missing something, though.

clang-tools-extra/clangd/ClangdServer.cpp
334–335

Why not do this before running rename? This would allow to:

  1. return result without doing the actual (expensive) rename if there's no identifier under the cursor
  2. reduce nesting of the (now large) large piece of code that runs rename:
auto Loc = getBeginningOfIdentifier(...);
auto Range = ...;
if (!Range)
  return llvm::None;
if (!CrossFileRename)
  return *Range; // FIXME: assume cross-file rename always succeeds
// do the rename...
349–359

Could we get a helper function (or constructor) to keep the call sites as simple as they used to be?
RenameInputs{Contents, File, NewName, Pos,...} should also work just fine and I don't think it hurts readability, since all the parameters here are named the same as the fields and most of them have different types.

356

This can always be obtained from the AST, right?
Why do we need it separately?

364

Maybe use llvm::joinErrors to combine multiple failures into a single error?
Should simplify the code.

clang-tools-extra/clangd/SourceCode.h
226

We should document this typedef, otherwise it does not provide much value.
A doc comment from ApplyEdits should fit just nicely here.

clang-tools-extra/clangd/refactor/Rename.h
23

What does it return for MainFilePath? llvm::None? MainFileCode?

ilya-biryukov added inline comments.Nov 4 2019, 3:00 AM
clang-tools-extra/clangd/refactor/Rename.cpp
114

NIT: in both rename modes

clang-tools-extra/clangd/refactor/Rename.h
33

MainFileCode can be obtained from the AST. Why not do it?

40

Why is MainFile special? Can it also be handled with the GetDirtyBuffer?

49

Why isn't AST part of the RenameInputs?

hokein updated this revision to Diff 227700.Nov 4 2019, 6:16 AM
hokein marked 13 inline comments as done.

address comments:

  • use VFS from AST;
  • simplify the interfaces;
clang-tools-extra/clangd/ClangdLSPServer.cpp
766

it is not redundant, we access the this->DraftMgr in the lambda body.

clang-tools-extra/clangd/ClangdServer.cpp
334–335

Good point.

356

yes, obtained from the AST.

364

I don't see using llvm::joinErrors can simplify the code here, joinErrors accepts two Error objects, and there is no way to create an empty Error object, we may end up with more code like:

llvm::Optional<llvm::Error> Err;
for (auto &E : *Edits) {
   if (auto E = reformatEdit(E.getValue(), Style)) {
      if (!Err) Err = std::move(E);
      else Err = joinErrors(std::move(*Err), std::move(Err));
   }
}
clang-tools-extra/clangd/refactor/Rename.h
33

ParsedAST doesn't provide such API to get the file code, I assume you mean InputsAndAST? The MainFileCode is from InputsAndAST.

40

I believe we should use the content from InputsAndAST, which is corresponding to the main AST (the content from GetDiryBuffer may not be reflected to the AST).

Build result: pass - 59816 tests passed, 0 failed and 762 were skipped.
Log files: console-log.txt, CMakeCache.txt

ilya-biryukov added inline comments.Nov 4 2019, 6:59 AM
clang-tools-extra/clangd/ClangdServer.cpp
357

NIT: inlining RInputs should save quite a few lines of code

364

Works nicely if you start with Error::success

auto Err = Error::success();
for (...) {
  Err = llvm::joinErrors(reformatEdit(E.getValue(), Style));
}
if (Err)
  return CB(std::move(Err));
clang-tools-extra/clangd/refactor/Rename.cpp
134

NIT: this comment is redundant, previous line says the same thing

150

Why isn't this a scope enum in the first place?

169

Why not a blacklist? I'd expect us to blacklist:

  • things with custom names (operators, constructors, etc)
  • virtual methods

Probably some things are missing from the list, but fundamentally most that have names are pretty similar, right?

173

Isn't the same true for local rename?

287

This behavior is very surprising... Obviously, returning empty results is incorrect and the callers have to handle the no-index case in a custom manner.

Maybe accept only non-null index and handle in the caller?

301

Again, it looks like the function returns incorrect results and the callers should actually handle the case where SymbolID cannot be obtained in a custom manner.

Maybe accept a SymbolID as a parameter instead of NamedDecl* and the callers handle this case gracefully?

321

This is not true anymore, right?
We should probably try getting to O(N) to avoid slowing things down

Could be a FIXME for now, but have to fix it soon.

clang-tools-extra/clangd/refactor/Rename.h
33

SourceManager has access to the input buffer.
StringRef MainFileCode = SM.getBufferData(SM.getMainFileID());

40

Does that mean we have three states of files that rename should be aware of?

  • Dirty buffer
  • Main file contents
  • Contents on disk

That definitely looks very complicated... Is there a chance we could abstract it away and just have a single source of truth for file contents?
I am thinking of a function that handles getting the file buffer by name, so we have all the complicated logic in one place. Would look something like:

std::function<std::string(PathRef /*Path*/)> GetFileContents;

Implementation could try to do the following in order:

  1. try to get contents of the main file.
  2. if failed, get contents from the dirty buffers.
  3. if failed, get contents from disk.

Could be a helper function accepting RenameInputs, but important to use it everywhere we get the file contents.

hokein updated this revision to Diff 227860.Nov 5 2019, 6:22 AM
hokein marked 13 inline comments as done.

address review comments.

hokein added inline comments.Nov 5 2019, 6:24 AM
clang-tools-extra/clangd/ClangdServer.cpp
364

thanks.

clang-tools-extra/clangd/refactor/Rename.cpp
150

this is tricky, the reason why I put it here and another copy below is to avoid regression of local rename (keep existing tests passed), if we move it in the first place, the error message of local rename may be changed, e.g. when renaming an external symbol (we know that from AST) without Index support, we'd like to return "used outside of the main file" rather than "no index provided".

thinking more about this, the no-index case is making our code complicated (within-file rename with no-index, cross-file rename with no-index), I believe the no-index case is not important, and is rare in practice, we could change this behavior, or assume the index is always provided, this would help to simplify the code, WDYT?

169

I believe our plan is to support major kinds of symbols (class, functions, enums, typedef, alias) , I feel scary to allow renaming nearly all NamedDecl (in theory, it should work but we're uncertain, for our experience of local rename, it sometimes leads to very surprising results). Having a whitelist can lead us to a better state, these symbols are officially supported.

173

no, actually the local rename does handle this, it finds all overriden methods from the AST, and updates all of them when doing rename.

287

moved the no-index handling to the caller, the caller code is still a bit fuzzy.

301

hmm, is this critical? I think it is safe to assume getSymbolID should always succeed, I believe we make this assumption in other code parts in clangd as well.

321

yes, I think it is not too bad to leave it now (as we limit the max number of affected number to 50), the idea to fix this is to build a line table. Updated the FIXME.

clang-tools-extra/clangd/refactor/Rename.h
40

yeah, having a single source for the file content could help, but I think this helper is internal-only.

Build result: fail - 59795 tests passed, 21 failed and 762 were skipped.

failed: lld.ELF/linkerscript/filename-spec.s
failed: Clang.Index/index-module-with-vfs.m
failed: Clang.Modules/double-quotes.m
failed: Clang.Modules/framework-public-includes-private.m
failed: Clang.VFS/external-names.c
failed: Clang.VFS/framework-import.m
failed: Clang.VFS/implicit-include.c
failed: Clang.VFS/include-mixed-real-and-virtual.c
failed: Clang.VFS/include-real-from-virtual.c
failed: Clang.VFS/include-virtual-from-real.c
failed: Clang.VFS/include.c
failed: Clang.VFS/incomplete-umbrella.m
failed: Clang.VFS/module-import.m
failed: Clang.VFS/module_missing_vfs.m
failed: Clang.VFS/real-path-found-first.m
failed: Clang.VFS/relative-path.c
failed: Clang.VFS/test_nonmodular.c
failed: Clang.VFS/umbrella-framework-import-skipnonexist.m
failed: Clang.VFS/vfsroot-include.c
failed: Clang.VFS/vfsroot-module.m
failed: Clang.VFS/vfsroot-with-overlay.c

Log files: console-log.txt, CMakeCache.txt

ilya-biryukov added inline comments.Nov 5 2019, 9:56 AM
clang-tools-extra/clangd/refactor/Rename.cpp
150

Agree, we shouldn't bother about the no-index case too much.
My comment here was referring to the lack of ReasonToReject:: qualifier of the name.

Maybe make ReasonToReject a scoped enum, i.e. enum class ReasonToReject rather than enum ReasonToReject?

169

I disagree here, I don't see how NamedDecl is special if its name is an identifier unless we something smart about it.
My recollection is that local rename issues come from the fact that there are certain kinds of references, which are not handled in the corresponding visitor.

In any case, I'd suggest blacklisting symbols we know are broken, e.g. IIRC we don't index references to namespaces, so they're definitely broken. And finding the issues with the rest of the symbols and fixing those.
In practice, this should improve both rename and cross-references - whenever we have broken cross-file rename, there's a good chance that the indexer is also broken.

173

Do we properly check overriden methods are not used outside the main file, though?
We might be assuming rename is "local" because there are not usages of the original function, but there may still be usages of the overriden function.

Worth adding a comment that local rename handles that through the AST.
Also useful to keep in mind that this is a regression, compared to the local rename (i.e. if we switch to cross-file rename by default, we'll break this use-case)

301

Should we maybe assert it succeeds instead?
If we make this assumption in the code, why not make it explicit?
If not, we should probably add tests where it fails.

321

Agree it's ok to keep as is for now.
But we should fix it before switching this as a default. It's not uncommon to have a single large file with lots of occurrences. We really don't have O(N^2) in those cases.

clang-tools-extra/clangd/refactor/Rename.h
40

I might be missing something, but it's in the public API of rename, right?

hokein updated this revision to Diff 228028.Nov 6 2019, 2:33 AM
hokein marked 5 inline comments as done.
  • use blacklist
  • change to enum scope
hokein added inline comments.Nov 6 2019, 2:35 AM
clang-tools-extra/clangd/refactor/Rename.cpp
150

ah, I see. Done.

169

Re-thinking this, you are right. Also, the indexable symbol kinds have a large overlap with the renamable symbol kinds. Changed to blacklist.

173

Do we properly check overriden methods are not used outside the main file, though?
We might be assuming rename is "local" because there are not usages of the original function, but there may still be usages of the overriden function.

No, we only check the original function, and we may encounter false positives, but we don't see any reports about this.

Worth adding a comment that local rename handles that through the AST.
Also useful to keep in mind that this is a regression, compared to the local rename (i.e. if we switch to cross-file rename by default, we'll break this use-case)

exactly, a way to avoid this regression when turning on cross-file rename by default is to first detect whether it is safe&sufficient to perform local rename, if yes, we just perform the local-rename.

clang-tools-extra/clangd/refactor/Rename.h
40

my concern is that choosing which source of the file content is an implementation detail and coupled with how the rename is implemented, why should the caller (ClangdServer?) create this complicated function?

Build result: fail - 59861 tests passed, 21 failed and 763 were skipped.

failed: lld.ELF/linkerscript/filename-spec.s
failed: Clang.Index/index-module-with-vfs.m
failed: Clang.Modules/double-quotes.m
failed: Clang.Modules/framework-public-includes-private.m
failed: Clang.VFS/external-names.c
failed: Clang.VFS/framework-import.m
failed: Clang.VFS/implicit-include.c
failed: Clang.VFS/include-mixed-real-and-virtual.c
failed: Clang.VFS/include-real-from-virtual.c
failed: Clang.VFS/include-virtual-from-real.c
failed: Clang.VFS/include.c
failed: Clang.VFS/incomplete-umbrella.m
failed: Clang.VFS/module-import.m
failed: Clang.VFS/module_missing_vfs.m
failed: Clang.VFS/real-path-found-first.m
failed: Clang.VFS/relative-path.c
failed: Clang.VFS/test_nonmodular.c
failed: Clang.VFS/umbrella-framework-import-skipnonexist.m
failed: Clang.VFS/vfsroot-include.c
failed: Clang.VFS/vfsroot-module.m
failed: Clang.VFS/vfsroot-with-overlay.c

Log files: console-log.txt, CMakeCache.txt

Thanks, this is in a good shape!
The only concern I have is the racy way to get dirty buffers, please see the corresponding comment.

clang-tools-extra/clangd/ClangdLSPServer.cpp
764

We should probably get a snapshot of all dirty buffers here instead.
A racy way (rename is run on a separate thread, new files updates might come in in the meantime) to get contents of the file looks like a bad idea, this will lead to hard-to-debug failures...

Note that ClangdServer also has access to all contents of all the files, they are stored in TUScheduler, so we don't need to pass GetDirtyBuffer callback up until the final run of rename

clang-tools-extra/clangd/ClangdServer.cpp
317

NIT: comment could be shortened to

/// "rename" is not valid at the position.

Or even removed.
Both would allow saving a line (if we put the comment on the same line as return

320

NIT: s/assueme/assume

also, in FIXME we normally give the final state, so it should probably be

/// FIXME: do not assume cross-file rename always succeeds
341–343

ClangdServer can obtain the dirty buffers via TUScheduler::getContents

hokein updated this revision to Diff 228213.Nov 7 2019, 4:26 AM
hokein marked 5 inline comments as done.
  • get dirty buffer in clangdServer layer;
  • save a snpatshot for all dirty buffer;
hokein added inline comments.Nov 7 2019, 4:27 AM
clang-tools-extra/clangd/ClangdLSPServer.cpp
764

thanks, I like the idea. This could simplify the code, with this approach, the dirty buffer of the main file would be the same as the one used for building the AST.

ilya-biryukov added inline comments.Nov 7 2019, 9:21 AM
clang-tools-extra/clangd/ClangdServer.cpp
343

NIT: s/SnapShot/Snapshot

372

NIT: return CB(std::move(*Edits));
to keep all calls to CB consistent.

clang-tools-extra/clangd/TUScheduler.h
186

NIT: seems like a leftover from the previous version. Maybe remove?

clang-tools-extra/clangd/refactor/Rename.cpp
61

NIT: this assert is redundant, Optional asserts it has a value when operator* is called.
The code could be simplified to the equivalent: Refs.insert(*getSymbolID(&D))

175

nit: add braces to the outer if

311

NIT: remove <>. should still work, right?

364

We don't seem to use it. Remove? Or am I missing the usage?

412

Why is this different from prepareRename, which does not call getMacroArgExpandedLocation?

446

NIT: remove <>

clang-tools-extra/clangd/refactor/Tweak.h
79–80

The comment is now redundant, since the typedef has the same comment.

clang-tools-extra/clangd/unittests/RenameTests.cpp
27

Could you document what this function is doing?

52

NIT: I suggest to call it applyEdits, there's nothing rename-specific in this function.

403–410

NIT: maybe simplify to EXPECT_EQ(applyRename(...).second, expectedResult(Code, NewName))?

hokein updated this revision to Diff 228294.Nov 7 2019, 1:12 PM
hokein marked 13 inline comments as done.

address comments.

hokein added inline comments.Nov 7 2019, 1:13 PM
clang-tools-extra/clangd/refactor/Rename.cpp
412

I didn't change it in this patch, but you raise a good point, prepareRename should call getMacroArgExpandedLocation.

ilya-biryukov accepted this revision.Nov 8 2019, 12:38 AM

LGTM.

It's probably worth collecting a list of things we need to fix before enabling cross-file rename and putting it somewhere (a GitHub issue, maybe?)
Important things that immediately come to mind:

  • add range-patching heuristics, measure how good they are
  • avoid O(N^2) when computing edits (converting from positions to offsets)
  • handle implicit references from the index

There are definitely more.

clang-tools-extra/clangd/refactor/Rename.cpp
412

Yep, makes sense to change it later. Could you put a FIXME, so that we don't forget about it?

This revision is now accepted and ready to land.Nov 8 2019, 12:38 AM
hokein updated this revision to Diff 229815.Nov 18 2019, 5:25 AM
hokein marked an inline comment as done.

rebase and call getMacroArgExpandedLocation in prepareRename.

hokein marked an inline comment as done.Nov 18 2019, 5:28 AM

LGTM.

It's probably worth collecting a list of things we need to fix before enabling cross-file rename and putting it somewhere (a GitHub issue, maybe?)
Important things that immediately come to mind:

  • add range-patching heuristics, measure how good they are
  • avoid O(N^2) when computing edits (converting from positions to offsets)
  • handle implicit references from the index

There are definitely more.

Thanks, filed at issues#193.

clang-tools-extra/clangd/refactor/Rename.cpp
412

Done, fixed this in this patch, it is trivial.

hokein updated this revision to Diff 230068.Nov 19 2019, 6:57 AM
hokein marked an inline comment as done.

Rebase.

Build result: pass - 60175 tests passed, 0 failed and 732 were skipped.
Log files: console-log.txt, CMakeCache.txt

This revision was automatically updated to reflect the committed changes.