-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[Refactoring] Add Codable refactoring action #41136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
@@ -1669,7 +1669,7 @@ deriveBodyDecodable_enum_init(AbstractFunctionDecl *initDecl, void *) { | |||
auto *nestedContainerDecl = createKeyedContainer( | |||
C, funcDC, C.getKeyedDecodingContainerDecl(), | |||
caseCodingKeys->getDeclaredInterfaceType(), | |||
VarDecl::Introducer::Var, C.Id_nestedContainer); | |||
VarDecl::Introducer::Let, C.Id_nestedContainer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The generated code would otherwise produce a warning:
Variable 'nestedContainer' was never mutated; consider changing to 'let' constant
The example in the doc comment of this method already said let nestedContainer
. So this looks like it was a small oversight (which didn't really affect anything so far, I imagine).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah looks like it may have been copy/pasted from deriveBodyEncodable_enum_encode
(which does need to be var
AFAIK, unlike this case)
let nestedContainer = try container.nestedContainer(keyedBy: Payload.PlainCodingKeys.self, forKey: Payload.CodingKeys.plain) | ||
|
||
self = Payload.plain(try nestedContainer.decode(String.self, forKey: Payload.PlainCodingKeys._0)) | ||
case .pair: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The indentation is a little weird here, but I don't think that's super important.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like it wouldn't be too hard to fix but... 🤷 See above, I wouldn't have bothered at all 😅
@@ -74,6 +74,7 @@ Action(llvm::cl::desc("kind:"), llvm::cl::init(RefactoringKind::None), | |||
"replace-bodies-with-fatalError", "Perform trailing closure refactoring"), | |||
clEnumValN(RefactoringKind::MemberwiseInitLocalRefactoring, "memberwise-init", "Generate member wise initializer"), | |||
clEnumValN(RefactoringKind::AddEquatableConformance, "add-equatable-conformance", "Add Equatable conformance"), | |||
clEnumValN(RefactoringKind::AddExplicitCodableImplementation, "add-explicit-codable-implementation", "Add Explicit Codable Implementation"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: this behaves different from the existing AddEquatableConformance
action. That is, for Codable we really want the synthesized implementation. This action doesn't make your type conform to Codable
(which is easy, just make it conform to it manually). It's geared towards "it conforms to Codable, and now I want to insert what the compiler generated".
@swift-ci please test |
|
||
self.firstName = try container.decode(String.self, forKey: User.CodingKeys.firstName) | ||
self.lastName = try container.decodeIfPresent(String.self, forKey: User.CodingKeys.lastName) | ||
return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The return
at the end is redundant of course. I'm not sure how to best handle that in the ASTPrinter
. (we'd have to check if it's the last statement in the scope, I suppose).
For now, I think it's fine to just keep it. It compiles, but looks a bit weird.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it implicit? If it isn't, could be be? And if so, can we skip printing implicit expressions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ended up adding a (very specific) SkipImplicitReturn
option to PrintOptions
. Skipping all implicit return
stmts made an existing test fail, so I wanted to minimize the potential breakage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I'm not sure skipping all implicit returns is correct, as implicit just means "created by the compiler", and we shouldn't skip an implicit return in e.g if condition { return }
, or one with an expression e.g return 0
. Would it be possible to narrow this behavior by having the AST printer only drop an implicit return stmt that is the last element of an AbstractFunctionDecl and has no expression?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
having the AST printer only drop an implicit return stmt that is the last element of an AbstractFunctionDecl and has no expression?
Ah, good idea. I've done that and removed the option I added (so it will just always skip the printing of such return
s).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very cool. Thanks for all the work in adding the expression printing too!
There's a few comments but they're all very minor. If you could split the expression printing fixes out into at least a separate commit that would be good, though it was pretty obvious what related to what in this case.
Is the refactoring only valid on the name of the container?
getInsertStartLoc() { | ||
SourceLoc MaxLoc = StartLoc; | ||
for (auto Mem : Range) { | ||
if (Mem->getEndLoc().getOpaquePointerValue() > |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SourceManager
has a isBeforeInBuffer
. Not static since it should be checking the two locations are in the same buffer, though that doesn't seem to be the case right now 🤔.
Another option would be to just use -1 the brace end loc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was taken from AddEquatableContext
, and I'd rather just keep this as-is. I don't think it has access to a SourceManager
here, which would make it slightly more involved to change it as you describe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be able to get it by jumping through a couple of pointers from the DeclContext
, e.g DC->getASTContext().SourceMgr
lib/IDE/Refactoring.cpp
Outdated
: Printer(Printer) {} | ||
|
||
bool walkToDeclPre(Decl *D) override { | ||
if (auto valueD = dyn_cast<ValueDecl>(D)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're also not very consistent with naming, but the current style guide is capitalized camel case (as much as I dislike it). The normal convention here would be eg. VD
.
Also add the *
or &
if the variable is a pointer/reference, makes it a little easier to read through.
lib/IDE/Refactoring.cpp
Outdated
bool walkToDeclPre(Decl *D) override { | ||
if (auto valueD = dyn_cast<ValueDecl>(D)) { | ||
if (!valueD->isSynthesized()) { | ||
return true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It also says not to use braces or single line ifs but again, there's not really any consistency here so... doesn't really matter.
lib/IDE/Refactoring.cpp
Outdated
return true; | ||
} | ||
SmallString<32> Scratch; | ||
auto name = valueD->getName().getString(Scratch); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naming again (there's probably others but I won't mention again).
Also it looks like name
is only used as a comparison here, in which case you could use VD->getName()->isCompoundName("init", "from")
instead.
ASTContext
also has known identifiers for all these, so you could use those. isCompoundName
doesn't have an overload to take ArrayRef<Identifier>
for the arguments though 🤷 .
And a third way is how DerivedConformanceCodable
does it 😆:
if (name.isCompoundName() && name.getBaseName() == ctx.Id_encode) {
auto argumentNames = name.getArgumentNames();
if (argumentNames.size() == 1 && argumentNames[0] == ctx.Id_to)
// ...
}
let nestedContainer = try container.nestedContainer(keyedBy: Payload.PlainCodingKeys.self, forKey: Payload.CodingKeys.plain) | ||
|
||
self = Payload.plain(try nestedContainer.decode(String.self, forKey: Payload.PlainCodingKeys._0)) | ||
case .pair: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like it wouldn't be too hard to fix but... 🤷 See above, I wouldn't have bothered at all 😅
let firstName: String | ||
let lastName: String? | ||
} | ||
// RUN: %refactor -add-explicit-codable-implementation -source-filename %s -pos=3:8 > %t.result/codable.swift |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alex added a nice refactor-check-compiles
that we use for the convert async refactorings. I also prefer CHECK
to output + diff, but maybe I'm the only one 😆
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Outputting each run to a separate file makes it very easy to spot bugs (and e.g. to copy the code). Having that all inline in comments would make it harder to understand, I think. 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not too bad for these, but for many refactorings most of the output doesn't actually matter. I guess this one has a lot compared to the file as a whole though. And personally I find copy pasting often misses bugs 🤷.
Anyway, I'm fine with the separate file, but I would like to check that it compiles.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but I would like to check that it compiles.
As we discussed offline, refactor-check-compiles
doesn't currently work when outputting to a file. That might be trivial to fix in the test infrastructure, but not something I want to attempt in this PR.
@swift-ci please test |
@@ -1669,7 +1669,7 @@ deriveBodyDecodable_enum_init(AbstractFunctionDecl *initDecl, void *) { | |||
auto *nestedContainerDecl = createKeyedContainer( | |||
C, funcDC, C.getKeyedDecodingContainerDecl(), | |||
caseCodingKeys->getDeclaredInterfaceType(), | |||
VarDecl::Introducer::Var, C.Id_nestedContainer); | |||
VarDecl::Introducer::Let, C.Id_nestedContainer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah looks like it may have been copy/pasted from deriveBodyEncodable_enum_encode
(which does need to be var
AFAIK, unlike this case)
getInsertStartLoc() { | ||
SourceLoc MaxLoc = StartLoc; | ||
for (auto Mem : Range) { | ||
if (Mem->getEndLoc().getOpaquePointerValue() > |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be able to get it by jumping through a couple of pointers from the DeclContext
, e.g DC->getASTContext().SourceMgr
/// Array of all conformed protocols | ||
SmallVector<swift::ProtocolDecl *, 2> Protocols; | ||
|
||
/// Range of internal members in declaration | ||
DeclRange Range; | ||
|
||
bool conformsToCodableProtocol() { | ||
for (ProtocolDecl *Protocol : Protocols) { | ||
if (Protocol->getKnownProtocolKind() == KnownProtocolKind::Encodable || | ||
Protocol->getKnownProtocolKind() == KnownProtocolKind::Decodable) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this use ModuleDecl::lookupConformance(Type, ...)
instead of searching the conformances manually? That would also let you dig out the init(from:)
or encode(to:)
witness and check them for isSynthesized
if we only want to provide this refactoring in cases where it's a synthesized conformance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, you're totally right. With this current implementation we'll provide the refactor option when Codable
is entirely manually implemented (so there's nothing synthesized).
I realize it's not ideal in this state, but I'd like to fix this in a follow up PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like it would appear but do nothing in this state. It's a fairly narrow condition anyway (only on a decl conforming to encodable/decodable), so implementing in a follow up sounds reasonable.
And fix operators not printing when we can't get a `MemberOperatorRef`.
lib/AST/ASTPrinter.cpp
Outdated
if (FD->hasBody()) { | ||
auto *Body = FD->getBody(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're walking the body here so it would be a bit odd that we don't have one, good to be safe though. But in that case I'd just check for if (auto *Body = FD->getBody())
instead (hasBody
just checks the kind).
Inserts the synthesized implementation. As part of this, fix some ASTPrinter bugs. rdar://87904700
@swift-ci please test |
Build failed |
@swift-ci please test linux |
Build failed |
if (auto metaType = expr->getType()->castTo<AnyMetatypeType>()) { | ||
// Don't print `.Type` for an expr. | ||
printType(metaType->getInstanceType()); | ||
} else { | ||
printType(expr->getType()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just noticed this condition won't actually fail as castTo
will never return a null type. Additionally it will crash if the TypeExpr
's type has not been set yet (which I don't think can be the case for the Codable synthesis in particular, but in general we should handle it).
We ought to print the instance type if it has been set, or else fallback to printing the TypeRepr
instead, e.g:
if (auto ty = expr->getInstanceType()) {
printType(ty);
} else {
expr->getTypeRepr()->print(Printer, Options);
}
I'm happy for this to be done in a follow-up though.
@swift-ci please test Linux platform |
Inserts the synthesized implementation.
As part of this, fix some ASTPrinter bugs.
rdar://87904700