Giter Club home page Giter Club logo

swiftpoet's Introduction

SwiftPoet

GitHub Workflow Status Maven Central Sonatype Nexus (Snapshots) codebeat badge

SwiftPoet is a Kotlin and Java API for generating .swift source files.

Source file generation can be useful when doing things such as annotation processing or interacting with metadata files (e.g., database schemas, protocol formats). By generating code, you eliminate the need to write boilerplate while also keeping a single source of truth for the metadata.

Example

Here's a HelloWorld file:

import RxSwift


class Greeter {

  private let name: String

  init(name: String) {
    self.name = name
  }

  func greet() -> Observable<String> {
    return Observable.from("Hello \(name)")
  }

}

And this is the code to generate it with SwiftPoet:

val observableTypeName = DeclaredTypeName.typeName("RxSwift.Observable")

val testClass = TypeSpec.classBuilder("Greeter")
   .addProperty("name", STRING, Modifier.PRIVATE)
   .addFunction(
      FunctionSpec.constructorBuilder()
         .addParameter("name", STRING)
         .addCode("self.name = name\n")
         .build()
   )
   .addFunction(
      FunctionSpec.builder("greet")
         .returns(observableTypeName.parameterizedBy(STRING))
         .addCode("return %T.from(\"Hello \\(name)\")\n", observableTypeName)
         .build()
   )
   .build()

val file = FileSpec.builder("Greeter")
   .addType(testClass)
   .build()

val out = StringWriter()
file.writeTo(out)

The KDoc catalogs the complete SwiftPoet API, which is inspired by JavaPoet.

Download

Download the latest .jar or depend via Maven:

<dependency>
  <groupId>io.outfoxx</groupId>
  <artifactId>swiftpoet</artifactId>
  <version>1.6.5</version>
</dependency>

or Gradle:

implementation 'io.outfoxx:swiftpoet:1.6.5'

Snapshots of the development version are available in Sonatype's snapshots repository.

License

Copyright 2017 Outfox, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

swiftpoet's People

Contributors

alexanderbliznyuk avatar dnkoutso avatar drochetti avatar efirestone avatar ema987 avatar foso avatar francybiga avatar jakewharton avatar kdk96 avatar kdubb avatar mattmook avatar oldergod avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

swiftpoet's Issues

1.4.1 crash

SwiftPoet 1.4.1 causes this crash. As the code base is very complex and I can't toString the FileSpec I can't easily share a reproducer, but from the stacktrace it's pretty clear what happens.
This worked fine in 1.4.0 and is caused by this change: 6da6707

Caused by: java.lang.IndexOutOfBoundsException: Empty list doesn't contain element at index 0.
	at kotlin.collections.EmptyList.get(Collections.kt:36)
	at kotlin.collections.EmptyList.get(Collections.kt:24)
	at io.outfoxx.swiftpoet.CodeWriter.emitTypeVariables(CodeWriter.kt:159)
	at io.outfoxx.swiftpoet.FunctionSpec.emitSignature(FunctionSpec.kt:115)
	at io.outfoxx.swiftpoet.FunctionSpec.emit$swiftpoet(FunctionSpec.kt:62)
	at io.outfoxx.swiftpoet.FunctionSpec.emit$swiftpoet$default(FunctionSpec.kt:42)
	at io.outfoxx.swiftpoet.ExtensionSpec.emit$swiftpoet(ExtensionSpec.kt:95)
	at io.outfoxx.swiftpoet.FileMemberSpec.emit$swiftpoet(FileMemberSpec.kt:41)
	at io.outfoxx.swiftpoet.FileSpec.emit(FileSpec.kt:96)
	at io.outfoxx.swiftpoet.FileSpec.writeTo(FileSpec.kt:52)
	at io.outfoxx.swiftpoet.FileSpec.writeTo(FileSpec.kt:68)
	at io.outfoxx.swiftpoet.FileSpec.writeTo(FileSpec.kt:73)

Including sub types within external extensions do not emit imports

Given test:

@Test
  @DisplayName("Generates correct imports for extension types ")
  fun testParentExtensionBug() {
    val parentElement = typeName(".Parent")
    val obsElement = typeName("RxSwift.Observable.Element")

    val extension =
      ExtensionSpec.builder(parentElement) // <----- using parentElement vs obsElement.enclosingTypeName()!! fails.
        .addFunction(
          FunctionSpec.builder("test")
            .returns(obsElement)
            .build()
        )
        .build()

    val testFile = FileSpec.builder("Test", "Test")
      .addExtension(extension)
      .build()

    val out = StringWriter()
    testFile.writeTo(out)

    assertThat(
      out.toString(),
      equalTo(
        """
            import RxSwift // <------ this is missing but should be there.

            extension Parent {

              func test() -> RxSwift.Observable.Element { // <---- this looks right if its fully qualified imo?
              }

            }

        """.trimIndent()
      )
    )
  }

Branch: https://github.com/outfoxx/swiftpoet/compare/extension_import_bug?expand=1

Seems related to #55

It seems when using obsElement as the extension type imports are emitted correctly which is why the test that was added in #55 passes correctly.

When removing:

if (typeSpec is ExternalTypeSpec) {
  return stackTypeName(i, simpleName)
}

Things start working, perhaps this is meant to check an additional condition? I am still getting familiarized with this code @kdubb

cc @lickel

Type bound is incorrectly generated in case of 2 or more bound types.

Consider the following code

TypeSpec
        .structBuilder("TestStruct")
        .addTypeVariable(
            TypeVariableName.typeVariable(
                "T",
                TypeVariableName.bound(Bound1),
                TypeVariableName.bound(Bound2)
            ),
        )
        .build()

the generated struct will be:

struct TestStruct<T> where T : Bound1 where T : Bound2 {
}

but expected:

struct TestStruct<T> where T : Bound1, T : Bound2 {
}

class only protocols

Hey, just tried to generate a class only protocol:

protocol Test: class { ... }

but i think this is not supported (correct me if i'm wrong) since swiftpoet treats the class keyword as a reserved keyword.

If you wan't i can put a PR together to fix this - Some thoughts:
We could just remove class from the reserved keywords and it would be possible but i first wanted to check with you if there would be a more elegant way to do it.
Another thought i had was to add a val unsafe: Boolean to the DeclaredTypeName constructor to skip the keyword check.

What do you think?

1.4.0 Release

Hi, while using swiftpoet 1.3.1 we stumbled upon the problem with public modifier on top level members, but we noticed it has already been fixed in #57.

Is there a chance that a 1.4.0 version will be released soon?

We would prefer to avoid forking for the only for producing our own artefacts and also we cannot check-in snapshot versions in our project. Thank you.

SwiftPoet 1.6.1 Update: Scope Issue in Nested Struct Generation

With SwiftPoet 1.6.1, there seems to be an issue with nested scope resolution.

Generating Code

FileSpec.builder("file")
  .addType(
    TypeSpec.structBuilder("A")
      .addFunction(
        FunctionSpec.constructorBuilder()
          .addParameter(
            ParameterSpec.builder(
              parameterName = "c",
              type = DeclaredTypeName.typeName("A.B.C"),
            ).build(),
          )
          .build(),
      )
      .addType(
        TypeSpec.structBuilder("B")
          .addType(
            TypeSpec.structBuilder("C")
              .build(),
          )
          .build(),
      )
      .build(),
  )
  .build()

Output of 1.6.1

This does not compile, as the c: C is not in the scope of A.

import A

struct A {

  init(c: C) {
  }

  struct B {

    struct C {
    }

  }

}

Output of 1.6.0

This successfully compiles

struct A {

  init(c: B.C) {
  }

  struct B {

    struct C {
    }

  }

}

Diff

0a1,2
> import A
>
3c5
<   init(c: B.C) {
---
>   init(c: C) {

Extensions don't handle imports

The import for extension types is not added using swiftpoet 1.3.1.

FileSpec.builder("file")
  .addExtension(
    ExtensionSpec.builder(DeclaredTypeName.typeName("RxSwift.Observable"))
      .build()
  )
  .build()

The generated code is:

extension RxSwift.Observable {
}

But it should be:

import RxSwift

extension RxSwift.Observable {
}

Publish GitHub releases

Useful for those of us who click "Watch" then "Releases only" in the GitHub UI to follow new releases.

Invalid rules for nested types and protocols

SwiftPoet implementation checks if the TypeSpec is a protocol when adding a nested type, however that rule is not 100% correct.

Here is a few scenarios that the current code can't handle correctly:

Scenario A: The nested type is a typealias

Protocols are allowed to have type aliases as nested types. Since TypeAliasSpec is a subclass of AnyTypeSpec the current implementation doesn't allow that. The following is a valid construct in Swift that cannot be built with SwiftPoet:

protocol MyProtocol {

  typealias NestedTypeAlias = String

}

Scenario B: Protocol is a nested type of a struct/class/enum

Protocols can't have nested types (other than type aliases) and cannot in any scenario be a nested type of another construct. The Swift compiler in such scenarios is Protocol 'Nested' cannot be nested inside another declaration. Therefore, the following code is possible to be built using SwiftPoet but it's invalid:

struct MyStruct {

  protocol MyNestedProtocol {
  }

}

Error when use ParameterSpec.unnamed - Char sequence is empty.

To create lambda argument i use:

FunctionTypeName.get(
  parameters = listOf(ParameterSpec.unnamed(STRING)),
  returnType = VOID
)

but on writeTo file i got error:

Caused by: java.util.NoSuchElementException: Char sequence is empty.
	at kotlin.text.StringsKt___StringsKt.first(_Strings.kt:71)
	at io.outfoxx.swiftpoet.UtilKt.escapeIfNotJavaIdentifier(Util.kt:136)
	at io.outfoxx.swiftpoet.UtilKt.escapeIfNecessary(Util.kt:138)
	at io.outfoxx.swiftpoet.ParameterSpec.emit$swiftpoet(ParameterSpec.kt:32)
	at io.outfoxx.swiftpoet.ParameterSpec.emit$swiftpoet$default(ParameterSpec.kt:31)
	at io.outfoxx.swiftpoet.ParameterSpecKt$emit$1.invoke(ParameterSpec.kt:130)
	at io.outfoxx.swiftpoet.ParameterSpecKt$emit$1.invoke(ParameterSpec.kt)
	at io.outfoxx.swiftpoet.ParameterSpecKt.emit(ParameterSpec.kt:146)
	at io.outfoxx.swiftpoet.ParameterSpecKt.emit$default(ParameterSpec.kt:130)
	at io.outfoxx.swiftpoet.FunctionTypeName.emit$swiftpoet(FunctionTypeName.kt:38)
	at io.outfoxx.swiftpoet.CodeWriter.emitCode(CodeWriter.kt:199)
	at io.outfoxx.swiftpoet.CodeWriter.emitCode(CodeWriter.kt:175)
	at io.outfoxx.swiftpoet.ParameterSpec.emit$swiftpoet(ParameterSpec.kt:37)
	at io.outfoxx.swiftpoet.FunctionSpec$emitSignature$1.invoke(FunctionSpec.kt:112)
	at io.outfoxx.swiftpoet.FunctionSpec$emitSignature$1.invoke(FunctionSpec.kt:20)
	at io.outfoxx.swiftpoet.ParameterSpecKt.emit(ParameterSpec.kt:150)
	at io.outfoxx.swiftpoet.ParameterSpecKt.emit$default(ParameterSpec.kt:130)
	at io.outfoxx.swiftpoet.FunctionSpec.emitSignature(FunctionSpec.kt:111)
	at io.outfoxx.swiftpoet.FunctionSpec.emit$swiftpoet(FunctionSpec.kt:63)
	at io.outfoxx.swiftpoet.FunctionSpec.emit$swiftpoet$default(FunctionSpec.kt:47)
	at io.outfoxx.swiftpoet.ExtensionSpec.emit$swiftpoet(ExtensionSpec.kt:90)
	at io.outfoxx.swiftpoet.FileMemberSpec.emit$swiftpoet(FileMemberSpec.kt:41)
	at io.outfoxx.swiftpoet.FileSpec.emit(FileSpec.kt:96)
	at io.outfoxx.swiftpoet.FileSpec.writeTo(FileSpec.kt:52)
	at io.outfoxx.swiftpoet.FileSpec.writeTo(FileSpec.kt:68)
	at io.outfoxx.swiftpoet.FileSpec.writeTo(FileSpec.kt:73)

to prevent it i use now:

FunctionTypeName.get(
  parameters = listOf(ParameterSpec.builder("_", STRING).build()),
  returnType = VOID
)

but need to fix logic in io.outfoxx.swiftpoet.ParameterSpec#emit

need to check if parameterName is blank.

Cannot emit backticked property

When trying to create the property, the name is first checked to be valid so ones like "extension" fail.

require(name.isName) { "not a valid name: $name" }

If I switch to manually backticking the name I get past this check but then upon emit it's has an additional set of backticks added to it.

codeWriter.emitCode("%L: %T", escapeIfNecessary(name), type)

This produces something like

struct Foo {
  let ``extension``: String
}

It seems like one or the other behaviors need removed. Either a property allows all names and then escapes with backticks if necessary, or it still does validation on the way in and then does not escape when emitting.

Preprocessor support

This is probably an insane feature to support fully, but perhaps we can distill targeted usage.

My sole use case is wrapping an entire extension like:

#if !FEATURE
extension Foo : Bar {}
#endif

Inferred type names are not finding "Swift." (amongst other things).

Updating Square/Wire from v1.3.1 => v1.5, I saw a regression where declared type names are regularly being expressed with their fully qualified name.

We're semi-frequently doing something to the effect of io.outfoxx.swiftpoet.STRING.makeOptional().makeNotOptional(); however, we're also directly DeclaredTypeName.typeName("Wire.ProtoWriter") in an extension, which similarly loses the understanding of Wire being imported.

It looks like only types used within an extension have this limitation?

square/wire#2454

SwiftPoet Status

Hello!

At Wire https://github.com/square/wire we rely heavily on SwiftPoet and we’re a bit anxious about the project’s state and its future. Would you be open to add one or two of us to it so we can make changes, fix bugs, and make some releases? Is SwiftPoet still getting support from you or someone else?
Worst case scenario for us would be to fork the project. We'd love to hear back from you; we're totally up to helping the project is possible.
Let us know

Operator overloading isn't supported.

Hi @kdubb ,
when I tried to generate operator func:

FunctionSpec
                    .builder("==")
                    .addModifiers(Modifier.STATIC)
                    .addParameter(ParameterSpec.builder("lhs", SelfTypeName.INSTANCE).build())
                    .addParameter(ParameterSpec.builder("rhs", SelfTypeName.INSTANCE).build())
                    .returns(BOOL)
                    .addCode("return true\n")
                    .build()

it produces :

  static func `==`(lhs: Self, rhs: Self) -> Bool {
    return true
  }

but `==` is incorrect func name in swift.

No ways to create @escaping lambda at all

To create lambda type i use FunctionTypeName.get but in all variants i can't pass escaping = true and as i see in sources - here no ways to change value after creation and also we can't use constructor from outside. so @escaping lamda can't be created, but logic for write this lambda - already done. need to add some way to change/set escaping flag

Support for optional closure/lambda parameters

Hello there,

I want to use generate the following function where the closure parameter is optional:

func test(closure: ((Swift.String, Swift.Int) -> Swift.String)?) {}

I tried it modifying a bit the already existent test in FunctionSpecTests using makeOptional(),

val closureTypeName =
      FunctionTypeName.get(listOf(ParameterSpec.unnamed(STRING), ParameterSpec.unnamed(INT)), STRING)
        .makeOptional()

but what I get is

func test(closure: (Swift.String, Swift.Int -> Swift.String?) {}

Furthermore, I tried also wanting the STRING return type as optional, so I changed the code to

    val closureTypeName =
      FunctionTypeName.get(listOf(ParameterSpec.unnamed(STRING), ParameterSpec.unnamed(INT)), STRING.makeOptional())
        .makeOptional()

and what I get is

func test(closure: (Swift.String, Swift.Int) -> Swift.String??) {}

If I'm not mistakenly using makeOptional() and I didn't miss something, it seems to me there is no way to emit the closure parameter between () to be able to mark it as optional using ? and the issue is in ParameterizedTypeName which always emits "%T?" without taking care of the parenthesis.

Could you check my PR for a solution proposal?

Update Documentation

  • References to Kotlin files and other incorrect terminology need to be updated or removed.
  • All classes need have at least basic documentation.
  • README should have more/better examples of Swift generation.

Add NameAllocator helper

This should be a pretty easy import. Was there a reason it was left out?

I'm running into problems with names from a schema conflicting with fixed names in the generated code. Name allocator is perfect for conditionally mangling names only when conflicts occur.

Do not indent switch when used in beginControlFlow

If the string starts with "switch " then do not automatically indent. Downside: we would have to maintain a boolean stack of calls to beginControlFlow such that on symmetric calls to nextControlFlow/endControlFlow know whether or not to unindent. As a result, this may not be worth doing! And that's fine.

This syntactical formatting oddity offends me, but apparently it's idiomatic. I just ported all my use of beginControlFlow for switch to addCode. I'm perfectly fine leaving it like that, but figured I'd file and see what others thought.

Formatting string produces error

We are using SwiftPoet on the Wire project to emit Swift code from proto files.

The test harness includes a proto file with a bunch of default values like this string:
https://github.com/square/wire/blob/master/wire-tests/src/commonTest/proto/kotlin/all_types.proto#L118

We read the value from the proto as a String and emit code:

CodeBlock.of("%S", defaultValue)

However the output code contains invalid characters and compilation fails. You can see the output here https://github.com/square/wire/pull/2514/files#diff-1b5612904d5c974962f1768754be37a013906a6ce05417992c9cbd14e97b81d4R255-R258

The error:

/Users/dnkoutso/Development/wire/wire-tests-swift/src/main/swift/AllTypes.swift:257:14: error: unprintable ASCII character found in source file
        
        ~güzel

KotlinPoet outputs https://github.com/square/wire/blob/master/wire-tests/src/commonTest/proto-kotlin/com/squareup/wire/protos/kotlin/alltypes/AllTypes.kt#L1991-L1992 which seems to be working.

Support `convenience init`

Hello I was wondering how following Swift code could be generated:

extension UIColor {
    public convenience init(hex: Int, alpha: CGFloat = 1) {
        let components = (
            r: CGFloat((hex >> 16) & 0xFF) / 255,
            g: CGFloat((hex >> 08) & 0xFF) / 255,
            b: CGFloat((hex >> 00) & 0xFF) / 255
        )
        self.init(red: components.r, green: components.g, blue: components.b, alpha: alpha)
    }
}

I tried following

.addExtension(
    ExtensionSpec.builder(uiColorType)
        .addFunction(
            FunctionSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .build()
        )
        .build()
)

Which yields the init function but no option to add the convenience keyword.

extension UIColor {
  public init() {
  }
}

PropertySpec builder DOES NOT add Modifiers to global properties

Having something like:

PropertySpec.builder(propertyName, STRING, Modifier.PUBLIC)
    .apply {
        this.initializer("\"$propertyValue\"")
    }
    .build()

What produces is:

let PropertyName: String = "property-name"

What is expected:

public let PropertyName: String = "property-name"

PropertySpec DOES NOT add Modifiers to global properties

Having something like:

PropertySpec.builder(propertyName, STRING, Modifier.PUBLIC)
    .apply {
        this.initializer("\"$propertyValue\"")
    }
    .build()

What produces is:

let PropertyName: String = "property-name"

What is expected:

public let PropertyName: String = "property-name"

Consider emitting "MARK" comments between code sections

Since the members of a class/struct/enum are emitted in a particular order, "MARK" comments could be produced between the sections to aid in navigation in xcode.

Something like

class Foo {

  // MARK Public Properties

  public var bar: Bar

  // MARK Private properties

  var baz: Baz

  // MARK Initializers

  ..

  // MARK Public functions

  ..

  // MARK Private functions

  ..
}

Better release procedure

We can make releasing easier:

  1. Disallow user tagging with SemVer (which is what causes a release).
  2. Create a YAML file (or something similar) that has the current released version.
  3. Add an action that detects changes to the YAML file and tags the commit that included the version change. This step will cause a release as it currently does but it will be guaranteed to be committed.

This would make the release procedure PR based and ensure everything is committed before generating a version tag; it has the added benefit of doing the tagging automatically.

Support Subscript builders

I wanted to add conformance to @dynamicMemberLookup in some generated code.
This is not possible as there's no way (that I can tell) to create subscripts.

Specifically, I wanted to create something like subscript<Property>(dynamicMember keyPath: KeyPath<BackingData, Property>) -> Property.

There doesn't seem to be any "escape hatch" for TypeSpec.Builder to just type "raw" Swift code.
As such, I think the only way to solve this would be a dedicated function/builder.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.