Skip to content

[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

Merged
merged 4 commits into from
Feb 4, 2022
Merged

[Refactoring] Add Codable refactoring action #41136

merged 4 commits into from
Feb 4, 2022

Conversation

louisdh
Copy link
Contributor

@louisdh louisdh commented Feb 1, 2022

Inserts the synthesized implementation.
As part of this, fix some ASTPrinter bugs.

rdar://87904700

@@ -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);
Copy link
Contributor Author

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).

Copy link
Contributor

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:
Copy link
Contributor Author

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.

Copy link
Contributor

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"),
Copy link
Contributor Author

@louisdh louisdh Feb 1, 2022

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".

@louisdh
Copy link
Contributor Author

louisdh commented Feb 1, 2022

@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
Copy link
Contributor Author

@louisdh louisdh Feb 1, 2022

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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 returns).

Copy link
Contributor

@bnbarham bnbarham left a 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() >
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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

: Printer(Printer) {}

bool walkToDeclPre(Decl *D) override {
if (auto valueD = dyn_cast<ValueDecl>(D)) {
Copy link
Contributor

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.

bool walkToDeclPre(Decl *D) override {
if (auto valueD = dyn_cast<ValueDecl>(D)) {
if (!valueD->isSynthesized()) {
return true;
Copy link
Contributor

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.

return true;
}
SmallString<32> Scratch;
auto name = valueD->getName().getString(Scratch);
Copy link
Contributor

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:
Copy link
Contributor

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
Copy link
Contributor

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 😆

Copy link
Contributor Author

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. 🙂

Copy link
Contributor

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.

Copy link
Contributor Author

@louisdh louisdh Feb 2, 2022

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.

@louisdh louisdh requested a review from bnbarham February 2, 2022 04:11
@louisdh
Copy link
Contributor Author

louisdh commented Feb 2, 2022

@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);
Copy link
Contributor

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() >
Copy link
Contributor

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

Comment on lines +3538 to +3552
/// 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;
}
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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`.
Comment on lines 4559 to 4560
if (FD->hasBody()) {
auto *Body = FD->getBody();
Copy link
Contributor

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
@louisdh
Copy link
Contributor Author

louisdh commented Feb 2, 2022

@swift-ci please test

@swift-ci
Copy link
Contributor

swift-ci commented Feb 3, 2022

Build failed
Swift Test Linux Platform
Git Sha - 3f3e643

@bnbarham
Copy link
Contributor

bnbarham commented Feb 3, 2022

@swift-ci please test linux

@swift-ci
Copy link
Contributor

swift-ci commented Feb 3, 2022

Build failed
Swift Test Linux Platform
Git Sha - 3f3e643

Comment on lines +4157 to +4162
if (auto metaType = expr->getType()->castTo<AnyMetatypeType>()) {
// Don't print `.Type` for an expr.
printType(metaType->getInstanceType());
} else {
printType(expr->getType());
}
Copy link
Contributor

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.

@bnbarham
Copy link
Contributor

bnbarham commented Feb 3, 2022

@swift-ci please test Linux platform

@bnbarham bnbarham merged commit ab85807 into swiftlang:main Feb 4, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants