diff --git a/.github/ISSUE_TEMPLATE/behavior-bug.yml b/.github/ISSUE_TEMPLATE/behavior-bug.yml index 8a870e00..6e69677e 100644 --- a/.github/ISSUE_TEMPLATE/behavior-bug.yml +++ b/.github/ISSUE_TEMPLATE/behavior-bug.yml @@ -1,42 +1,58 @@ name: Behavior Bug -description: Report issues with plugin incompatibility or other behavior related issues. +description: Report issues with invalid code generation or other behavior bugs +title: "[Bug]: " labels: [ "status: needs triage", "type: bug" ] body: - - type: textarea + - type: markdown + attributes: + value: | + Thank you for reporting a bug to DartPoet! + Please fill out the information below to help us understand the issue. + - type: markdown + attributes: + value: | + Before filling the form fields, please consider the following: + - Search for existing issues in the [issue tracker](https://github.com/theEvilReaper/DartPoet/issues) + - type: input attributes: - label: Expected behavior - description: What you expected to see. + label: DartPoet version + description: Please enter the version of DartPoet you are using. + placeholder: 1.0.0 validations: - required: true - + required: false - type: textarea attributes: - label: Actual behavior - description: What you actually saw. + label: Describe the bug + description: | + A clear and concise description of what the bug is. + If you have a screenshot of the bug, please attach it below. validations: required: true - - type: textarea attributes: - label: Steps to reproduce - description: This may include a video, or detailed instructions to help reconstruct the issue. + label: Steps to reproduce the bug + description: Tell us exactly how to reproduce the bug you are experiencing + placeholder: | + 1. ... + 2. ... + 3. ... validations: required: true - - type: textarea attributes: - label: Other + label: Code sample description: | - Please include other helpful information below. - The more information we receive, the quicker and more effective we can be at finding the solution to the issue. - validations: - required: false - - - type: markdown - attributes: + Please create a reproducible sample to show us the bug in action and attach it below between the lines with the backticks. + This helps us to verify that the bug is valid and prevents us from having to ask you for a sample later. + + Without this we will unlikely be able to progress on the issue, and because of that + we regretfully will have to close it. + + **Note**: Please do not upload screenshots of text. Instead, use code blocks + or the above mentioned ways to upload your code sample. value: | - Before submitting this issue, please ensure the following: - - 1. You searched for and ensured there isn't already an open issue regarding this. - 2. Your version of kotlin/java is supported by DartPoet. - 3. You are running the latest version of DartPoet from [our github page](https://github.com/theEvilReaper/DartPoet). + ```kotlin + [Paste your code here] + ``` + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..43608360 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +blank_issues_enabled: false +contact_links: + - name: DartPoet discussions + url: https://github.com/theEvilReaper/DartPoet/discussions + about: If you have any questions or problems, please use the discussions section + emoji: ❓ diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 0b599f4d..584817ce 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -1,11 +1,12 @@ name: Feature Request description: Suggest an idea for DartPoet +title: "[Feature Request]: " labels: [ "status: needs triage", "type: feature" ] body: - type: markdown attributes: value: | - Thank you for filling out a feature request for SolarSystem! Please be as detailed as possible so that we may consider and review the request easier. + Thank you for filling out a feature request for DartPoet! Please be as detailed as possible so that we may consider and review the request easier. We ask that you search all the issues to avoid a duplicate feature request. If one exists, please reply if you have anything to add. Before requesting a new feature, please make sure you are using the latest version and that the feature you are requesting is not already in DartPoet. diff --git a/.github/workflows/maven-central.yml b/.github/workflows/maven-central.yml new file mode 100644 index 00000000..aaeee6c3 --- /dev/null +++ b/.github/workflows/maven-central.yml @@ -0,0 +1,40 @@ + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# GitHub recommends pinning actions to a commit SHA. +# To get a newer version, you will need to update the SHA. +# You can also reference a tag or branch, but the action may change without warning. + +name: Publish package to the Maven Central Repository +on: + push: + branches: + - main + - develop + release: + types: [created] +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - name: Set up Java + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'adopt' + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v2 + - name: Publish package + uses: gradle/gradle-build-action@v2 + with: + arguments: publishMavenJavaPublicationToMavenRepository + env: + OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + ORG_GRADLE_PROJECT_signingKey: ${{ secrets.PGP_SECRET }} + ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.PGP_PASSPHRASE }} \ No newline at end of file diff --git a/.github/workflows/test-pr.yml b/.github/workflows/test-pr.yml new file mode 100644 index 00000000..da1f9d0d --- /dev/null +++ b/.github/workflows/test-pr.yml @@ -0,0 +1,24 @@ +name: Test PR +on: [pull_request] +jobs: + build_pr: + if: github.repository_owner == 'theEvilReaper' + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - name: Validate Gradle Wrapper + uses: gradle/wrapper-validation-action@v2 + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + - name: Build with ${{ matrix.os }} + run: | + git config --global user.email "no-reply@github.com" + git config --global user.name "Github Actions" + ./gradlew test diff --git a/README.md b/README.md index 97781c1d..cf8a4b86 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # DartPoet +[![license](https://img.shields.io/github/license/theEvilReaper/DartPoet?style=for-the-badge&color=b2234c)](../LICENSE) + # **DartPoet is still in development and not ready for production use.** ## Description diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..034e8480 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/build.gradle.kts b/build.gradle.kts index bf16450c..56593c1d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,12 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { + signing + `maven-publish` + `java-library` alias(libs.plugins.kotlin.jvm) - application - id("org.jetbrains.changelog") version "2.0.0" + alias(libs.plugins.changelog) + alias(libs.plugins.dokka) } group = "net.theevilreaper.dartpoet" @@ -12,28 +17,48 @@ repositories { } dependencies { - testImplementation(libs.truth) - testImplementation(libs.kotlin.junit) + compileOnly(libs.jetbrains.annotations) + testImplementation(kotlin("test")) + testImplementation(libs.google.truth) + testImplementation(libs.junit.api) + testImplementation(libs.junit.params) + testRuntimeOnly(libs.junit.engine) } tasks { + test { useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + } } compileKotlin { - kotlinOptions { - jvmTarget = "17" - useK2 = true + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) } } } -kotlin { - jvmToolchain(17) +val sourceJar by tasks.register("kotlinJar") { + from(sourceSets.main.get().allSource) + archiveClassifier.set("sources") } -application { - mainClass.set("MainKt") +val dokkaJavadocJar by tasks.register("dokkaHtmlJar") { + dependsOn(rootProject.tasks.dokkaHtml) + from(rootProject.tasks.dokkaHtml.flatMap { it.outputDirectory }) + archiveClassifier.set("html-docs") +} + +val dokkaHtmlJar by tasks.register("dokkaJavadocJar") { + dependsOn(rootProject.tasks.dokkaJavadoc) + from(rootProject.tasks.dokkaJavadoc.flatMap { it.outputDirectory }) + archiveClassifier.set("javadoc") +} + +kotlin { + jvmToolchain(17) } changelog { @@ -42,4 +67,68 @@ changelog { keepUnreleasedSection.set(true) unreleasedTerm.set("[Unreleased]") groups.set(listOf("Added", "Changed", "Deprecated", "Removed", "Fixed", "Security")) -} \ No newline at end of file +} + +publishing { + publications { + create("mavenJava") { + from(components.findByName("java")) + groupId = "dev.themeinerlp" + artifactId = "dartpoet" + version = rootProject.version.toString() + artifact(dokkaJavadocJar) + artifact(dokkaHtmlJar) + artifact(sourceJar) + pom { + name.set("DartPoet") + description.set("A Kotlin API which allows the generation of code for dart") + url.set("https://github.com/theEvilReaper/DartPoet") + licenses { + license { + name.set("AGPL-3.0") + url.set("https://github.com/theEvilReaper/DartPoet/blob/develop/LICENSE") + } + } + issueManagement { + system.set("Github") + url.set("https://github.com/theEvilReaper/DartPoet/issues") + } + developers { + developer { + id.set("themeinerlp") + name.set("Phillipp Glanz") + email.set("p.glanz@madfix.me") + } + developer { + id.set("theEvilReaper") + name.set("Steffen Wonning") + email.set("steffenwx@gmail.com") + } + } + scm { + connection.set("scm:git@github.com:theEvilReaper/DartPoet.git") + developerConnection.set("scm:git@github.com:theEvilReaper/DartPoet.git") + url.set("https://github.com/theEvilReaper/DartPoet") + } + } + } + } + repositories { + maven { + val releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" + val snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/" + url = if (version.toString().endsWith("SNAPSHOT")) uri(snapshotsRepoUrl) else uri(releasesRepoUrl) + credentials { + username = System.getenv("OSSRH_USERNAME") + password = System.getenv("OSSRH_PASSWORD") + } + } + } +} + +signing { + val signingKey: String? by project + val signingPassword: String? by project + useInMemoryPgpKeys(signingKey, signingPassword) + sign(publishing.publications) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml deleted file mode 100644 index 6c14d505..00000000 --- a/gradle/libs.versions.toml +++ /dev/null @@ -1,13 +0,0 @@ - -[versions] - -kotlin = "1.8.10" -googleTruth = "1.1.3" - -[plugins] -kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } - -[libraries] - -truth = { group = "com.google.truth", name="truth", version.ref = "googleTruth" } -kotlin-junit = { group = "org.jetbrains.kotlin", name = "kotlin-test-junit", version.ref = "kotlin" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba77..7f93135c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bdc9a83b..a80b22ce 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 index 79a61d42..1aa94a42 --- a/gradlew +++ b/gradlew @@ -83,10 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f..6689b85b 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,92 +1,92 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..39a2b6e9 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ] +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 3e461bc6..e8582a54 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,17 @@ - rootProject.name = "DartPoet" +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + version("junit", "5.10.2") + library("google.truth", "com.google.truth", "truth").version("1.4.0") + library("junit.api", "org.junit.jupiter", "junit-jupiter-api").versionRef("junit") + library("junit.engine", "org.junit.jupiter", "junit-jupiter-engine").versionRef("junit") + library("junit.params", "org.junit.jupiter", "junit-jupiter-params").versionRef("junit") + library("jetbrains.annotations", "org.jetbrains", "annotations").version("24.1.0") + plugin("changelog", "org.jetbrains.changelog").version("2.2.0") + plugin("dokka", "org.jetbrains.dokka").version("1.9.10") + plugin("kotlin.jvm", "org.jetbrains.kotlin.jvm").version("1.9.22") + } + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/DartClass.kt b/src/main/kotlin/net/theevilreaper/dartpoet/DartClass.kt deleted file mode 100644 index 9ca37c3d..00000000 --- a/src/main/kotlin/net/theevilreaper/dartpoet/DartClass.kt +++ /dev/null @@ -1,51 +0,0 @@ -package net.theevilreaper.dartpoet - -import net.theevilreaper.dartpoet.import.DartImport -import net.theevilreaper.dartpoet.writer.CodeWriter - -class DartClass( - val builder: Builder -) { - - internal fun emit( - codeWriter: CodeWriter - ) { - } - - - class Builder internal constructor( - val className: String, - val classType: DartClassType - ) { - - internal var imports: MutableList = mutableListOf() - - fun addImport(dartImport: DartImport): Builder { - this.imports += dartImport - return this - } - - fun addImports(dartImports: Iterable): Builder { - this.imports += dartImports - return this - } - - fun addImports(dartImport: () -> Iterable): Builder { - this.imports += dartImport() - return this - } - - fun build(): DartClass { - require(className.trim().isEmpty()) { "The class name can't be empty" } - - return DartClass(this) - } - } - - companion object { - - @JvmStatic fun classBuilder(className: String) = Builder(className, DartClassType.CLASS) - } - - -} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/DartClassType.kt b/src/main/kotlin/net/theevilreaper/dartpoet/DartClassType.kt deleted file mode 100644 index f3a348c4..00000000 --- a/src/main/kotlin/net/theevilreaper/dartpoet/DartClassType.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.theevilreaper.dartpoet - -enum class DartClassType( - val keyWord: String, - val defaultModifier: Array -) { - CLASS("class", arrayOf()), - ENUM("enum", arrayOf()), - MIXIN("mixin", arrayOf()), -} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/DartFile.kt b/src/main/kotlin/net/theevilreaper/dartpoet/DartFile.kt new file mode 100644 index 00000000..9138a787 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/DartFile.kt @@ -0,0 +1,153 @@ +package net.theevilreaper.dartpoet + +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import net.theevilreaper.dartpoet.clazz.ClassSpec +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.buildCodeString +import net.theevilreaper.dartpoet.code.writer.DartFileWriter +import net.theevilreaper.dartpoet.directive.DartDirective +import net.theevilreaper.dartpoet.directive.ExportDirective +import net.theevilreaper.dartpoet.directive.LibraryDirective +import net.theevilreaper.dartpoet.directive.PartDirective +import net.theevilreaper.dartpoet.directive.RelativeDirective +import net.theevilreaper.dartpoet.extension.ExtensionSpec +import net.theevilreaper.dartpoet.function.FunctionSpec +import net.theevilreaper.dartpoet.util.* +import net.theevilreaper.dartpoet.property.consts.ConstantPropertySpec +import net.theevilreaper.dartpoet.util.DART_FILE_ENDING +import net.theevilreaper.dartpoet.util.isDartConventionFileName +import net.theevilreaper.dartpoet.util.toImmutableList +import java.io.IOException +import java.io.OutputStreamWriter +import java.lang.Appendable +import java.nio.file.Files +import java.nio.file.Path + +class DartFile internal constructor( + builder: DartFileBuilder +) { + internal val name: String = builder.name + internal val indent: String = builder.indent + internal val annotations: List = builder.annotations.toImmutableList() + internal val types: List = builder.specTypes.toImmutableList() + internal val extensions: List = builder.extensionStack + internal val docs = builder.docs + internal val constants: Set = builder.constants.toImmutableSet() + + private val directives = builder.directives.toImmutableList() + + internal val dartImports = + DirectiveOrdering.sortDirectives(DartDirective::class, directives) { it.contains("dart:") } + internal val packageImports = + DirectiveOrdering.sortDirectives(DartDirective::class, directives) { it.contains("package:") } + internal val partImports = DirectiveOrdering.sortDirectives(PartDirective::class, directives) + internal val libImport = DirectiveOrdering.sortDirectives(LibraryDirective::class, directives) + internal val exportDirectives = + DirectiveOrdering.sortDirectives(ExportDirective::class, directives) + internal val relativeImports = + DirectiveOrdering.sortDirectives(RelativeDirective::class, directives) + + internal val typeDefs = builder.typeDefs.toImmutableList() + internal val hasTypeDefs = typeDefs.isNotEmpty() + + init { + check(name.trim().isNotEmpty()) { "The name of a class can't be empty (ONLY SPACES ARE NOT ALLOWED" } + if (libImport.isNotEmpty()) { + check(libImport.size == 1) { "Only one library directive is allowed" } + } + } + + internal fun write(codeWriter: CodeWriter) { + DartFileWriter().write(this, codeWriter) + } + + override fun toString() = buildCodeString { write(this) } + + internal val callEmit: (Any, CodeWriter) -> Unit = { o: Any, c: CodeWriter -> + emitInternal(o, c) + } + + private fun emitInternal(o: Any, c: CodeWriter) { + when (o::class) { + FunctionSpec::class -> { + callEmit(o, c) + } + } + } + + /** + * Writes the content from a [DartFile] to the given [Appendable]. + * @param out the [Appendable] where the content should be written + * @throws IOException if the content can't be written + */ + @Throws(IOException::class) + fun write(out: Appendable) { + val codeWriter = CodeWriter( + out, + indent = indent + ) + write(codeWriter) + codeWriter.close() + } + + /** + * Writes the content from a [DartFile] to the given [Path]. + * @param path the path where the file should be written + * @throws IOException if the file can't be written + */ + @Throws(IOException::class) + fun write(path: Path) { + require(Files.notExists(path) || Files.isDirectory(path)) { + "The given path $path exists but it is not a directory" + } + + require(isDartConventionFileName(name)) { + """ + The given name $name has some issues with the naming + Please take a look at this page https://dart.dev/tools/linter-rules#file_names + """.trimIndent() + } + + val fileName = if (name.endsWith(DART_FILE_ENDING)) { + name + } else { + "$name$DART_FILE_ENDING" + } + + val outPutPath = path.resolve(fileName) + OutputStreamWriter(Files.newOutputStream(outPutPath), Charsets.UTF_8) + .use { writer -> write(writer) } + } + + /** + * Converts a [DartFile] to a [DartFileBuilder] instance. + * @return the created [DartFileBuilder] instance + */ + fun toBuilder(): DartFileBuilder { + val builder = DartFileBuilder(this.name) + builder.specTypes.addAll(this.types) + builder.directives.addAll(this.directives) + builder.annotations.addAll(this.annotations) + builder.extensionStack.addAll(this.extensions) + builder.constants.addAll(this.constants) + builder.docs.addAll(this.docs.toMutableList()) + builder.indent = this.indent + return builder + } + + /** + * Creates a new instance of [DartFileBuilder] with the specified name. + */ + companion object { + + /** + * Creates a new instance from an [DartFileBuilder] to create class structures. + * @param name the name which is used for the dart file + * @return the created instance from the [DartFileBuilder] instance + */ + @JvmStatic + fun builder(name: String): DartFileBuilder { + return DartFileBuilder(name) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/DartFileBuilder.kt b/src/main/kotlin/net/theevilreaper/dartpoet/DartFileBuilder.kt new file mode 100644 index 00000000..59b616c9 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/DartFileBuilder.kt @@ -0,0 +1,121 @@ +package net.theevilreaper.dartpoet + +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import net.theevilreaper.dartpoet.clazz.ClassBuilder +import net.theevilreaper.dartpoet.clazz.ClassSpec +import net.theevilreaper.dartpoet.code.CodeBlock +import net.theevilreaper.dartpoet.extension.ExtensionSpec +import net.theevilreaper.dartpoet.directive.Directive +import net.theevilreaper.dartpoet.function.typedef.TypeDefSpec +import net.theevilreaper.dartpoet.property.consts.ConstantPropertySpec +import net.theevilreaper.dartpoet.property.PropertySpec +import net.theevilreaper.dartpoet.util.DEFAULT_INDENT +import net.theevilreaper.dartpoet.util.isIndent + +class DartFileBuilder( + val name: String +) { + internal val docs: MutableList = mutableListOf() + internal val specTypes: MutableList = mutableListOf() + internal val directives: MutableList = mutableListOf() + internal val annotations: MutableList = mutableListOf() + internal val extensionStack: MutableList = mutableListOf() + internal val constants: MutableSet = mutableSetOf() + internal val typeDefs: MutableList = mutableListOf() + internal var indent = DEFAULT_INDENT + + /** + * Add a constant [PropertySpec] to the file. + * @param constant the property to add + */ + fun constant(constant: ConstantPropertySpec) = apply { + this.constants += constant + } + + fun constants(vararg constants: ConstantPropertySpec) = apply { + this.constants += constants + } + + fun directive(directive: Directive) = apply { + this.directives += directive + } + + fun directive(directive: () -> Directive) = apply { + this.directives += directive() + } + + fun directives(vararg directive: Directive) = apply { + this.directives += directive + } + + fun doc(format: String, vararg args: Any) = apply { + this.docs.add(CodeBlock.of(format.replace(' ', '·'), *args)) + } + + fun indent(indent: String) = apply { + check(isIndent(indent)) { "An indent can only contains only spaces" } + this.indent = indent + } + + fun indent(indent: () -> String) = apply { + this.indent(indent()) + } + + fun extension(extension: ExtensionSpec) = apply { + this.extensionStack += extension + } + + fun extension(extension: () -> ExtensionSpec) = apply { + this.extensionStack += extension() + } + + fun extensions(vararg extensions: ExtensionSpec) = apply { + this.extensionStack += extensions + } + + /** + * Add a type definition to the file builder. + * @param typeDef the type definition to add + * @return the current instance of [DartFileBuilder] + */ + fun typeDef(typeDef: TypeDefSpec) = apply { + this.typeDefs += typeDef + } + + /** + * Add an array of type definitions to the file builder. + * @param typeDef the type definitions to add + * @return the current instance of [DartFileBuilder] + */ + fun typeDef(vararg typeDef: TypeDefSpec) = apply { + this.typeDefs += typeDef + } + + fun type(dartFileSpec: ClassSpec) = apply { + this.specTypes += dartFileSpec + } + + fun type(vararg classSpecs: ClassSpec) = apply { + this.specTypes += classSpecs + } + + fun type(dartFileSpec: ClassBuilder) = apply { + this.specTypes += dartFileSpec.build() + } + + fun annotations(vararg annotations: AnnotationSpec) = apply { + this.annotations += annotations + } + + fun annotation(annotation: AnnotationSpec) = apply { + this.annotations += annotation + } + + /** + * Creates a new reference from the [DartFile] class. + * @return the created instance + */ + fun build(): DartFile { + return DartFile(this) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/DartModifier.kt b/src/main/kotlin/net/theevilreaper/dartpoet/DartModifier.kt index 8632944e..115b46b9 100644 --- a/src/main/kotlin/net/theevilreaper/dartpoet/DartModifier.kt +++ b/src/main/kotlin/net/theevilreaper/dartpoet/DartModifier.kt @@ -1,15 +1,40 @@ package net.theevilreaper.dartpoet +/** + * The enum contains all modifiers which are exists in the programming language dart. + * @author theEvilReaper + * @since 1.0.0 + */ enum class DartModifier( internal val identifier: String, private vararg val modifiers: ModifierTarget ) { - + PUBLIC("", ModifierTarget.CLASS, ModifierTarget.PROPERTY, ModifierTarget.FUNCTION), PRIVATE("_", ModifierTarget.FUNCTION, ModifierTarget.PROPERTY), + STATIC("static", ModifierTarget.FUNCTION, ModifierTarget.PROPERTY), LATE("late", ModifierTarget.PROPERTY), - FINAL("final", ModifierTarget.PROPERTY); + FINAL("final", ModifierTarget.PROPERTY), + WITH("with", ModifierTarget.CLASS), + ASYNC("async", ModifierTarget.FUNCTION), + CONST("const", ModifierTarget.PROPERTY), + EXTENSION("extension", ModifierTarget.CLASS), + ENUM("enum", ModifierTarget.CLASS), + MIXIN("mixin", ModifierTarget.CLASS), + ABSTRACT("abstract", ModifierTarget.CLASS), + FACTORY("factory", ModifierTarget.FUNCTION), + CLASS("class", ModifierTarget.CLASS), + LIBRARY("library", ModifierTarget.CLASS), + ON("on", ModifierTarget.CLASS), + TYPEDEF("typedef", ModifierTarget.TYPEDEF), + DYNAMIC("dynamic", ModifierTarget.PARAMETER), + REQUIRED("required", ModifierTarget.PARAMETER), + VOID("void", ModifierTarget.INTERFACE, ModifierTarget.FUNCTION); + /** + * Checks if an [ModifierTarget] is present in a specific [DartModifier]. + * @param modifierTarget the [ModifierTarget] to test + */ internal fun containsTarget(modifierTarget: ModifierTarget): Boolean { return modifiers.contains(modifierTarget) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/FileSpec.kt b/src/main/kotlin/net/theevilreaper/dartpoet/FileSpec.kt deleted file mode 100644 index 0f991bbb..00000000 --- a/src/main/kotlin/net/theevilreaper/dartpoet/FileSpec.kt +++ /dev/null @@ -1,4 +0,0 @@ -package net.theevilreaper.dartpoet - -class FileSpec { -} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/FileSpecBuilder.kt b/src/main/kotlin/net/theevilreaper/dartpoet/FileSpecBuilder.kt deleted file mode 100644 index b49576c1..00000000 --- a/src/main/kotlin/net/theevilreaper/dartpoet/FileSpecBuilder.kt +++ /dev/null @@ -1,49 +0,0 @@ -package net.theevilreaper.dartpoet - -import net.theevilreaper.dartpoet.annotation.AnnotationSpec -import net.theevilreaper.dartpoet.meta.SpecData -import net.theevilreaper.dartpoet.meta.SpecMethods - -class FileSpecBuilder( - val name: String -): SpecMethods { - - internal val specData = SpecData() - - - override fun annotations(annotations: Iterable) = apply { - this.specData.annotations(annotations) - } - - override fun annotations(annotations: () -> Iterable) = apply { - this.specData.annotations(annotations) - } - - override fun annotation(annotation: () -> AnnotationSpec) = apply { - this.specData.annotation(annotation) - } - - override fun annotation(annotation: AnnotationSpec) = apply { - this.specData.annotation(annotation) - } - - override fun modifier(modifier: DartModifier) = apply { - this.specData.modifier(modifier) - } - - override fun modifier(modifier: () -> DartModifier) = apply { - this.specData.modifier(modifier) - } - - override fun modifiers(modifiers: Iterable) = apply { - this.specData.modifiers(modifiers) - } - - override fun modifiers(modifiers: () -> Iterable) = apply { - this.specData.modifiers(modifiers) - } - - fun build(): FileSpec { - return FileSpec() - } -} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/InheritKeyword.kt b/src/main/kotlin/net/theevilreaper/dartpoet/InheritKeyword.kt new file mode 100644 index 00000000..e315b19c --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/InheritKeyword.kt @@ -0,0 +1,14 @@ +package net.theevilreaper.dartpoet + +/** + * Contains all available inherit options from the programming language dart. + * @author theEvilReaper + * @since 1.0.0 + */ +enum class InheritKeyword( + val identifier: String +) { + MIXIN(DartModifier.WITH.identifier), + EXTENDS("extends"), + IMPLEMENTS("implements") +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/ModifierTarget.kt b/src/main/kotlin/net/theevilreaper/dartpoet/ModifierTarget.kt index fca8319e..9773f915 100644 --- a/src/main/kotlin/net/theevilreaper/dartpoet/ModifierTarget.kt +++ b/src/main/kotlin/net/theevilreaper/dartpoet/ModifierTarget.kt @@ -1,11 +1,16 @@ package net.theevilreaper.dartpoet +/** + * The enum declaration defines on which target a [DartModifier] can be used. + * @since 1.0.0 + * @author theEvilReaper + */ internal enum class ModifierTarget { CLASS, FUNCTION, INTERFACE, PROPERTY, - PARAMETER - -} \ No newline at end of file + PARAMETER, + TYPEDEF +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/annotation/AnnotationSpec.kt b/src/main/kotlin/net/theevilreaper/dartpoet/annotation/AnnotationSpec.kt index 55f0babe..ee6ed485 100644 --- a/src/main/kotlin/net/theevilreaper/dartpoet/annotation/AnnotationSpec.kt +++ b/src/main/kotlin/net/theevilreaper/dartpoet/annotation/AnnotationSpec.kt @@ -1,11 +1,109 @@ package net.theevilreaper.dartpoet.annotation +import net.theevilreaper.dartpoet.code.* +import net.theevilreaper.dartpoet.code.writer.AnnotationWriter +import net.theevilreaper.dartpoet.type.ClassName +import net.theevilreaper.dartpoet.type.TypeName +import net.theevilreaper.dartpoet.type.asClassName +import net.theevilreaper.dartpoet.util.toImmutableSet +import kotlin.reflect.KClass /** + * The [AnnotationSpec] class encapsulates essential data that defines a metadata / annotation structure. + * Annotations can be used in Dart to add additional information to the code base. + * Typically, an annotation starts with the character @, followed by an identifier, + * and optionally with meta information which are encapsulated in round brackets. + * It's important to note that you can't really use the predefined annotations from the JDK or Kotlin because the languages are not interoperable to Dart, + * + * Common annotations from Dart: + * - @deprecated, + * - @override + * - @pragma + * @param builder the builder instance to retrieve the data from * @author theEvilReaper * @version 1.0.0 - * @since - **/ + * @since 1.0.0 + */ +class AnnotationSpec( + builder: AnnotationSpecBuilder, +) { + internal val typeName: TypeName = builder.typeName + internal val content: Set = builder.content.toImmutableSet() + internal val hasContent: Boolean = content.isNotEmpty() + internal val hasMultipleContentParts: Boolean = content.size > 1 -class AnnotationSpec { -} \ No newline at end of file + /** + * Triggers the write process for an [AnnotationSpec] object. + * @param codeWriter the [CodeWriter] instance to write the spec to + * @param inline if the spec should be written inline + */ + internal fun write( + codeWriter: CodeWriter, + inline: Boolean = true, + ) { + AnnotationWriter().emit(this, codeWriter, inline = inline) + } + + /** + * Returns a string representation of the [AnnotationSpec]. + * The method triggers the [write] method to get the spec object as string + * @return the created string representation + */ + override fun toString() = buildCodeString { write(this) } + + /** + * Creates a new [AnnotationSpecBuilder] reference from an existing [AnnotationSpec] object. + * @return the created [AnnotationSpecBuilder] instance + */ + fun toBuilder(): AnnotationSpecBuilder { + val builder = AnnotationSpecBuilder(this.typeName) + builder.content.addAll(this.content) + return builder + } + + /** + * The companion object contains some helper methods to create a new instance from the [AnnotationSpecBuilder]. + */ + companion object { + + /** + * Creates a new instance from the [AnnotationSpecBuilder]. + * @param name the name for the annotation provided as [String] + * @return the created instance from the [AnnotationSpecBuilder] + */ + @JvmStatic + fun builder(name: String) = AnnotationSpecBuilder(ClassName(name)) + + /** + * Creates a new instance from the [AnnotationSpecBuilder]. + * @param type the type for the annotation provided as [ClassName] + * @return the created instance from the [AnnotationSpecBuilder] + */ + @JvmStatic + fun builder(type: ClassName) = AnnotationSpecBuilder(type) + + /** + * Creates a new instance from the [AnnotationSpecBuilder]. + * @param type the type for the annotation provided as [TypeName] + * @return the created instance from the [AnnotationSpecBuilder] + */ + @JvmStatic + fun builder(type: TypeName) = AnnotationSpecBuilder(type) + + /** + * Creates a new instance from the [AnnotationSpecBuilder]. + * @param type the type for the annotation provided as [Class] + * @return the created instance from the [AnnotationSpecBuilder] + */ + @JvmStatic + fun builder(type: Class<*>) = AnnotationSpecBuilder(type.asClassName()) + + /** + * Creates a new instance from the [AnnotationSpecBuilder]. + * @param type the type for the annotation provided as [KClass] + * @return the created instance from the [AnnotationSpecBuilder] + */ + @JvmStatic + fun builder(type: KClass) = AnnotationSpecBuilder(type.asClassName()) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/annotation/AnnotationSpecBuilder.kt b/src/main/kotlin/net/theevilreaper/dartpoet/annotation/AnnotationSpecBuilder.kt index 482ccbe4..69bb2260 100644 --- a/src/main/kotlin/net/theevilreaper/dartpoet/annotation/AnnotationSpecBuilder.kt +++ b/src/main/kotlin/net/theevilreaper/dartpoet/annotation/AnnotationSpecBuilder.kt @@ -1,15 +1,57 @@ package net.theevilreaper.dartpoet.annotation +import net.theevilreaper.dartpoet.code.CodeBlock +import net.theevilreaper.dartpoet.type.TypeName /** + * With the AnnotationBuilder data can be set to an annotation. + * These are later read out during generation and converted into code accordingly. * @author theEvilReaper + * @param typeName the name of the annotation * @version 1.0.0 * @since **/ +class AnnotationSpecBuilder( + internal val typeName: TypeName +) { + /** + * Stores the content parts from the annotation. + */ + internal val content: MutableList = mutableListOf() -class AnnotationSpecBuilder { + /** + * Add a content part to the annotation. + * @param format the format string + * @param args the arguments for the format string + * @return the given instance of an [AnnotationSpecBuilder] + */ + fun content(format: String, vararg args: Any) = apply { + content(CodeBlock.of(format, *args)) + } + + /** + * Add a content part to the annotation. + * @param codeFragment the code fragment to add provided as [CodeBlock] + * @return the given instance of an [AnnotationSpecBuilder] + */ + fun content(codeFragment: CodeBlock) = apply { + this.content += codeFragment + } + + /** + * Add a content part to the annotation. + * @param codeFragment the code fragment to add provided as lambda block + * @return the given instance of an [AnnotationSpecBuilder] + */ + fun content(codeFragment: () -> CodeBlock) = apply { + this.content += codeFragment() + } + /** + * Creates a new instance from the [AnnotationSpec]. + * @return the created instance + */ fun build(): AnnotationSpec { - return AnnotationSpec() + return AnnotationSpec(this) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/clazz/ClassBuilder.kt b/src/main/kotlin/net/theevilreaper/dartpoet/clazz/ClassBuilder.kt new file mode 100644 index 00000000..78fe8b41 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/clazz/ClassBuilder.kt @@ -0,0 +1,261 @@ +package net.theevilreaper.dartpoet.clazz + +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.InheritKeyword +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import net.theevilreaper.dartpoet.enum.EnumPropertySpec +import net.theevilreaper.dartpoet.function.FunctionSpec +import net.theevilreaper.dartpoet.meta.SpecData +import net.theevilreaper.dartpoet.meta.SpecMethods +import net.theevilreaper.dartpoet.function.constructor.ConstructorSpec +import net.theevilreaper.dartpoet.function.typedef.TypeDefSpec +import net.theevilreaper.dartpoet.property.PropertySpec +import net.theevilreaper.dartpoet.property.consts.ConstantPropertySpec +import net.theevilreaper.dartpoet.type.TypeName +import net.theevilreaper.dartpoet.type.asTypeName +import java.lang.reflect.Type +import kotlin.reflect.KClass + +//TODO: Add check to prevent illegal modifiers on some class combinations +/** + * The [ClassBuilder] is the entry point to describe all relevant object structures which are needed to generate a class. + * + * @since 1.0.0 + */ +class ClassBuilder internal constructor( + internal val name: String?, + internal val classType: ClassType, + vararg modifiers: DartModifier +) : SpecMethods { + internal val classMetaData: SpecData = SpecData(*modifiers) + internal val isAnonymousClass get() = name == null && classType == ClassType.CLASS + internal val isEnumClass get() = classType == ClassType.ENUM + internal val isMixinClass get() = classType == ClassType.MIXIN + internal val isAbstract get() = classType == ClassType.ABSTRACT + internal val isLibrary get() = classType == ClassType.CLASS + internal val constructorStack: MutableList = mutableListOf() + internal val propertyStack: MutableList = mutableListOf() + internal val functionStack: MutableList = mutableListOf() + internal val enumPropertyStack: MutableList = mutableListOf() + internal val constantStack: MutableSet = mutableSetOf() + internal val typedefs: MutableList = mutableListOf() + internal var superClass: TypeName? = null + internal var inheritKeyWord: InheritKeyword? = null + internal var endWithNewLine = false + + /** + * Add a constant [PropertySpec] to the file. + * @param constant the property to add + */ + fun constant(constant: ConstantPropertySpec) = apply { + this.constantStack += constant + } + + /** + * Add an array of constant [PropertySpec] to the file. + * @param constants the array to add + */ + fun constants(vararg constants: ConstantPropertySpec) = apply { + this.constantStack += constants + } + + /** + * Add a [TypeDefSpec] to the spec. + * @param typeDefSpec the typedef to add + */ + fun typedef(typeDefSpec: TypeDefSpec) = apply { + this.typedefs += typeDefSpec + } + + /** + * Add an array of [TypeDefSpec] to the spec. + * @param typeDefSpec the typedefs to add + */ + fun typedef(vararg typeDefSpec: TypeDefSpec) = apply { + this.typedefs += typeDefSpec + } + + /** + * Add a [EnumPropertySpec] to the spec. + * @param enumPropertySpec the property to add + */ + fun enumProperty(enumPropertySpec: EnumPropertySpec) = apply { + require(classType == ClassType.ENUM) { "Only a enum class can have enum properties" } + this.enumPropertyStack += enumPropertySpec + } + + /** + * Add an array of [EnumPropertySpec] to the spec. + * @param properties the properties to add + */ + fun enumProperties(vararg properties: EnumPropertySpec) = apply { + require(classType == ClassType.ENUM) { "Only a enum class can have enum properties" } + this.enumPropertyStack += properties + } + + /** + * Set the class from which the generated class should inherit. + * @param superClass the name from the super class as [TypeName] + * @param inheritKeyword the keyword to use for the inheritance + * @return the given instance of an [ClassBuilder] + */ + fun superClass(superClass: TypeName, inheritKeyword: InheritKeyword) = apply { + this.superClass = superClass + inheritKeyWord = inheritKeyword + } + + /** + * Set the class from which the generated class should inherit. + * @param superClass the name from the super class as [Type] + * @param inheritKeyword the keyword to use for the inheritance + * @return the given instance of an [ClassBuilder] + */ + fun superClass(superClass: Type, inheritKeyword: InheritKeyword) = apply { + this.superClass = superClass.asTypeName() + inheritKeyWord = inheritKeyword + } + + /** + * Set the class from which the generated class should inherit. + * @param superClass the name from the super class as [KClass] + * @param inheritKeyword the keyword to use for the inheritance + * @return the given instance of an [ClassBuilder] + */ + fun superClass(superClass: KClass<*>, inheritKeyword: InheritKeyword) = apply { + this.superClass = superClass.asTypeName() + inheritKeyWord = inheritKeyword + } + + /** + * Indicates if the class should end with an empty line. + * @param endWithNewLine True for a new line at the end otherwise false + */ + fun endWithNewLine(endWithNewLine: Boolean) = apply { + this.endWithNewLine = endWithNewLine + } + + /** + * Add a [PropertySpec] to the class builder. + * @param propertySpec the property to add + * @return the given instance of an [ClassBuilder] + */ + fun property(propertySpec: PropertySpec) = apply { + this.propertyStack += propertySpec + } + + /** + * Add a [PropertySpec] to the class builder over a lambda reference. + * @param propertySpec the property to add + * @return the given instance of an [ClassBuilder] + */ + fun property(propertySpec: () -> PropertySpec) = apply { + this.propertyStack += propertySpec() + } + + /** + * Add an array of [PropertySpec] to the class builder. + * @param properties the properties to add + * @return the given instance of an [ClassBuilder] + */ + fun properties(vararg properties: PropertySpec) = apply { + this.propertyStack += properties + } + + /** + * Add a [FunctionSpec] to the class builder. + * @param function the function to add + * @return the given instance of an [ClassBuilder] + */ + fun function(function: FunctionSpec) = apply { + this.functionStack += function + } + + /** + * Add a [FunctionSpec] to the class builder over a lambda reference. + * @param function the function to add + * @return the given instance of an [ClassBuilder] + */ + fun function(function: () -> FunctionSpec) = apply { + this.functionStack += function() + } + + /** + * Add a [ConstructorSpec] to the class builder. + * @param constructor the constructor to add + * @return the given instance of an [ClassBuilder] + */ + fun constructor(constructor: ConstructorSpec) = apply { + this.constructorStack += constructor + } + + /** + * Add a [ConstructorSpec] to the class builder over a lambda reference. + * @param constructor the constructor to add + * @return the given instance of an [ClassBuilder] + */ + fun constructor(constructor: () -> ConstructorSpec) = apply { + this.constructorStack += constructor() + } + + /** + * Add a [AnnotationSpec] to the class builder. + * @param annotation the annotation to add + * @return the given instance of an [ClassBuilder] + */ + override fun annotation(annotation: AnnotationSpec) = apply { + this.classMetaData.annotation(annotation) + } + + /** + * Add a [AnnotationSpec] to the class builder over a lambda reference. + * @param annotation the annotation to add + * @return the given instance of an [ClassBuilder] + */ + override fun annotation(annotation: () -> AnnotationSpec) = apply { + this.classMetaData.annotation(annotation) + } + + /** + * Add an array of [AnnotationSpec] to the class builder. + * @param annotations the annotations to add + * @return the given instance of an [ClassBuilder] + */ + override fun annotations(vararg annotations: AnnotationSpec) = apply { + this.classMetaData.annotations(*annotations) + } + + /** + * Add a [DartModifier] value to the class builder. + * @param modifier the modifier to add + * @return the given instance of an [ClassBuilder] + */ + override fun modifier(modifier: DartModifier) = apply { + this.classMetaData.modifier(modifier) + } + + /** + * Add a [DartModifier] value to the class builder over a lambda reference. + * @param modifier the modifier to add + * @return the given instance of an [ClassBuilder] + */ + override fun modifier(modifier: () -> DartModifier) = apply { + this.classMetaData.modifier(modifier) + } + + /** + * Add an array of [DartModifier] values to the class builder. + * @param modifiers the modifiers to add + * @return the given instance of an [ClassBuilder] + */ + override fun modifiers(vararg modifiers: DartModifier) = apply { + this.classMetaData.modifiers(*modifiers) + } + + /** + * Creates a new instance from the [ClassSpec]. + * @return the created instance + */ + fun build(): ClassSpec { + return ClassSpec(this) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/clazz/ClassSpec.kt b/src/main/kotlin/net/theevilreaper/dartpoet/clazz/ClassSpec.kt new file mode 100644 index 00000000..77093b65 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/clazz/ClassSpec.kt @@ -0,0 +1,134 @@ +package net.theevilreaper.dartpoet.clazz + +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.DartModifier.CLASS +import net.theevilreaper.dartpoet.DartModifier.WITH +import net.theevilreaper.dartpoet.InheritKeyword +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.buildCodeString +import net.theevilreaper.dartpoet.code.writer.ClassWriter +import net.theevilreaper.dartpoet.enum.EnumPropertySpec +import net.theevilreaper.dartpoet.function.FunctionSpec +import net.theevilreaper.dartpoet.function.constructor.ConstructorSpec +import net.theevilreaper.dartpoet.function.typedef.TypeDefSpec +import net.theevilreaper.dartpoet.property.PropertySpec +import net.theevilreaper.dartpoet.type.TypeName +import net.theevilreaper.dartpoet.util.toImmutableList +import net.theevilreaper.dartpoet.util.toImmutableSet + +/** + * A [ClassBuilder] describes the actual content of the class. + * The content includes functions, typedefs, const values etc. + * Partly some things are also only allowed to be set on certain classes. + * @param builder the [ClassBuilder] instance to retrieve data from it + * @since 1.0.0 + * @author theEvilReaper + */ +class ClassSpec internal constructor( + builder: ClassBuilder, +) { + internal val name: String = builder.name.orEmpty() + internal val classType: ClassType = builder.classType + internal val modifiers: Set = builder.classMetaData.modifiers.toImmutableSet() + internal val annotations: Set = builder.classMetaData.annotations.toImmutableSet() + internal val endsWithNewLine: Boolean = builder.endWithNewLine + internal val isEnum: Boolean = builder.isEnumClass + internal val isAbstract: Boolean = builder.isAbstract + internal val isMixin: Boolean = builder.isMixinClass + internal val isAnonymous: Boolean = builder.isAnonymousClass + internal val isLibrary: Boolean = builder.isLibrary + internal val superClass: TypeName? = builder.superClass + internal val inheritKeyWord: InheritKeyword? = builder.inheritKeyWord + internal val classModifiers: Set = modifiers.filter { it != WITH }.toImmutableSet() + internal val typeDefs: List = builder.typedefs.toImmutableList() + internal val functions: Set = builder.functionStack.toImmutableSet() + internal val properties: Set = builder.propertyStack.toImmutableSet() + internal val constructors: Set = builder.constructorStack.toImmutableSet() + internal val enumPropertyStack: List = builder.enumPropertyStack.toImmutableList() + internal var constantStack = builder.constantStack.toImmutableSet() + + /** + * Returns true when the class has no content to generate. + */ + internal val hasNoContent: Boolean + get() = functions.isEmpty() && properties.isEmpty() && constructors.isEmpty() && constantStack.isEmpty() && enumPropertyStack.isEmpty() + + init { + if (!isLibrary) { + check(name.isNotEmpty()) { "The name of a class can't be empty" } + } + + if (isEnum) { + check(enumPropertyStack.isNotEmpty()) { "A enum requires at least one enum property" } + + val propertiesSize: Int = properties.size + + enumPropertyStack.forEach { + check(it.parameters.size == propertiesSize) { "The entries from the enum property must have the same size" } + } + } + } + + /** + * Calls the [ClassWriter] to write the data from the spec into code for dart + * @param codeWriter the [CodeWriter] instance to apply the data + */ + internal fun write(codeWriter: CodeWriter) { + ClassWriter().write(this, codeWriter) + } + + /** + * Returns a [String] representation from the class spec. + * @return the generated representation as string + */ + override fun toString() = buildCodeString { write(this) } + + /** + * The class contains methods to create a new [ClassBuilder] instance for a specific class. + */ + companion object { + + /** + * Create a new [ClassBuilder] instance for a normal dart class. + * @return the created instance + */ + @JvmStatic + fun builder(name: String) = ClassBuilder(name, ClassType.CLASS, CLASS) + + /** + * Create a new [ClassBuilder] instance for an anonymous dart class. + * @return the created instance + */ + @JvmStatic + fun anonymousClassBuilder() = ClassBuilder(null, ClassType.CLASS) + + /** + * Create a new [ClassBuilder] instance for an enum dart class. + * @return the created instance + */ + @JvmStatic + fun enumClass(name: String) = ClassBuilder(name, ClassType.ENUM) + + /** + * Create a new [ClassBuilder] instance for a mixin dart class. + * @return the created instance + */ + @JvmStatic + fun mixinClass(name: String) = ClassBuilder(name, ClassType.MIXIN) + + /** + * Create a new [ClassBuilder] instance for an abstract dart class. + * @return the created instance + */ + @JvmStatic + fun abstractClass(name: String) = ClassBuilder(name, ClassType.ABSTRACT) + + /** + * Creates a new [ClassBuilder] instance for a library class. + * @return the created instance + */ + @JvmStatic + fun libraryClass(name: String) = ClassBuilder(name, ClassType.LIBRARY) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/clazz/ClassType.kt b/src/main/kotlin/net/theevilreaper/dartpoet/clazz/ClassType.kt new file mode 100644 index 00000000..875c579d --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/clazz/ClassType.kt @@ -0,0 +1,19 @@ +package net.theevilreaper.dartpoet.clazz + +import net.theevilreaper.dartpoet.DartModifier + +/** + * The [ClassType] enum defines all class variants which are currently available in the programming language Dart. + * @author theEvilReaper + * @version 1.0.0 + * @since 1.0.0 + **/ +enum class ClassType( + internal val keyword: String, +) { + CLASS(DartModifier.CLASS.identifier), + ABSTRACT(DartModifier.ABSTRACT.identifier), + MIXIN(DartModifier.MIXIN.identifier), + ENUM(DartModifier.ENUM.identifier), + LIBRARY(DartModifier.LIBRARY.identifier) +} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/clazz/DartClassBuilder.kt b/src/main/kotlin/net/theevilreaper/dartpoet/clazz/DartClassBuilder.kt deleted file mode 100644 index 0ec9fb5b..00000000 --- a/src/main/kotlin/net/theevilreaper/dartpoet/clazz/DartClassBuilder.kt +++ /dev/null @@ -1,65 +0,0 @@ -package net.theevilreaper.dartpoet.clazz - -import net.theevilreaper.dartpoet.DartClassType -import net.theevilreaper.dartpoet.DartModifier -import net.theevilreaper.dartpoet.annotation.AnnotationSpec -import net.theevilreaper.dartpoet.meta.SpecData -import net.theevilreaper.dartpoet.meta.SpecMethods -import net.theevilreaper.dartpoet.property.DartPropertySpec - -class DartClassBuilder internal constructor( - internal val name: String?, - internal val classType: DartClassType -): SpecMethods { - - private val classMetaData: SpecData = SpecData() - private val isAnonymousClass get() = name == null && classType == DartClassType.CLASS - private val isEnumClass get() = classType == DartClassType.ENUM - private val isMixinClass get() = classType == DartClassType.MIXIN - - private val propertyStack: MutableList = mutableListOf() - - fun property(dartPropertySpec: DartPropertySpec) = apply { - this.propertyStack += dartPropertySpec - } - - fun property(dartPropertySpec: () -> DartPropertySpec) = apply { - this.propertyStack += dartPropertySpec() - } - - override fun annotations(annotations: Iterable)= apply { - this.classMetaData.annotations(annotations) - } - - override fun annotations(annotations: () -> Iterable) = apply { - this.classMetaData.annotations(annotations) - } - - override fun annotation(annotation: () -> AnnotationSpec) = apply { - this.classMetaData.annotation(annotation) - } - - override fun annotation(annotation: AnnotationSpec) = apply { - this.classMetaData.annotation(annotation) - } - - override fun modifier(modifier: DartModifier) = apply { - this.classMetaData.modifier(modifier) - } - - override fun modifier(modifier: () -> DartModifier) = apply { - this.classMetaData.modifier(modifier) - } - - override fun modifiers(modifiers: Iterable) = apply { - this.classMetaData.modifiers(modifiers) - } - - override fun modifiers(modifiers: () -> Iterable) = apply { - this.classMetaData.modifiers(modifiers) - } - - fun build(): DartClassSpec { - return DartClassSpec(this) - } -} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/clazz/DartClassSpec.kt b/src/main/kotlin/net/theevilreaper/dartpoet/clazz/DartClassSpec.kt deleted file mode 100644 index 0a2959f2..00000000 --- a/src/main/kotlin/net/theevilreaper/dartpoet/clazz/DartClassSpec.kt +++ /dev/null @@ -1,42 +0,0 @@ -package net.theevilreaper.dartpoet.clazz - -import net.theevilreaper.dartpoet.DartClassType - -class DartClassSpec internal constructor( - builder: DartClassBuilder -) { - - internal val name = builder.name - internal val classType = builder.classType - - - - - - companion object { - - /** - * Create a new [DartClassBuilder] instance for a normal dart class. - * @return the created instance - */ - @JvmStatic fun builder(name: String): DartClassBuilder = DartClassBuilder(name, DartClassType.CLASS) - - /** - * Create a new [DartClassBuilder] instance for an anonymous dart class. - * @return the created instance - */ - @JvmStatic fun anonymousClassBuilder(): DartClassBuilder = DartClassBuilder( null, DartClassType.CLASS) - - /** - * Create a new [DartClassBuilder] instance for a enum dart class. - * @return the created instance - */ - @JvmStatic fun enumClass(name: String): DartClassBuilder = DartClassBuilder(name, DartClassType.ENUM) - - /** - * Create a new [DartClassBuilder] instance for a mixin dart class. - * @return the created instance - */ - @JvmStatic fun mixinClass(name: String): DartClassBuilder = DartClassBuilder(name, DartClassType.MIXIN) - } -} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/CodeBlock.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/CodeBlock.kt new file mode 100644 index 00000000..6dc3f3d4 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/code/CodeBlock.kt @@ -0,0 +1,519 @@ +/* + * Copyright (C) 2015 Square, 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 + * + * https://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. + * + * Changes: + * - removed parts which doesnt work for dart + */ +@file:JvmName("CodeBlocks") + +package net.theevilreaper.dartpoet.code + +import net.theevilreaper.dartpoet.type.TypeName +import net.theevilreaper.dartpoet.type.asTypeName +import net.theevilreaper.dartpoet.util.EMPTY_STRING +import net.theevilreaper.dartpoet.util.escapeIfNecessary +import net.theevilreaper.dartpoet.util.isOneOf +import net.theevilreaper.dartpoet.util.toImmutableList +import java.lang.reflect.Type +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import kotlin.reflect.KClass + +/** + * A fragment of a .kt file, potentially containing declarations, statements, and documentation. + * Code blocks are not necessarily well-formed Kotlin code, and are not validated. This class + * assumes kotlinc will check correctness later! + * + * Code blocks support placeholders like [java.text.Format]. This class primarily uses a percent + * sign `%` but has its own set of permitted placeholders: + * + * * `%L` emits a *literal* value with no escaping. Arguments for literals may be strings, + * primitives, [type declarations][TypeSpec], [annotations][AnnotationSpec] and even other code + * blocks. + * * `%N` emits a *name*, using name collision avoidance where necessary. Arguments for names may + * be strings (actually any [character sequence][CharSequence]), [parameters][ParameterSpec], + * [properties][PropertySpec], [functions][FunSpec], and [types][TypeSpec]. + * * `%S` escapes the value as a *string*, wraps it with double quotes, and emits that. For + * example, `6" sandwich` is emitted `"6\" sandwich"`. `%S` will also escape all dollar signs + * (`$`), use `%P` for string templates. + * * `%P` - Similar to `%S`, but doesn't escape dollar signs (`$`) to allow creation of string + * templates. If the string contains dollar signs that should be escaped - use `%S`. + * * `%T` emits a *type* reference. Types will be imported if possible. Arguments for types may be + * [classes][Class]. + * * `%M` emits a *member* reference. A member is either a function or a property. If the member is + * importable, e.g. it's a top-level function or a property declared inside an object, the import + * will be resolved if possible. Arguments for members must be of type [MemberName]. + * * `%%` emits a percent sign. + * * `·` emits a space that never wraps. KotlinPoet prefers to wrap lines longer than 100 columns. + * It does this by replacing normal spaces with a newline and indent. Note that spaces in strings + * are never wrapped. + * * `⇥` increases the indentation level. + * * `⇤` decreases the indentation level. + * * `«` begins a statement. For multiline statements, every line after the first line is + * double-indented. + * * `»` ends a statement. + */ +class CodeBlock private constructor( + internal val formatParts: List, + internal val args: List, +) { + /** A heterogeneous list containing string literals and value placeholders. */ + + fun isEmpty(): Boolean = formatParts.isEmpty() + + fun isNotEmpty(): Boolean = !isEmpty() + + /** + * Returns a code block with `prefix` stripped off, or null if this code block doesn't start with + * `prefix`. + * + * This is a pretty basic implementation that might not cover cases like mismatched whitespace. We + * could offer something more lenient if necessary. + */ + internal fun withoutPrefix(prefix: CodeBlock): CodeBlock? { + if (formatParts.size < prefix.formatParts.size) return null + if (args.size < prefix.args.size) return null + + var prefixArgCount = 0 + var firstFormatPart: String? = null + + // Walk through the formatParts of prefix to confirm that it's a of this. + prefix.formatParts.forEachIndexed { index, formatPart -> + if (formatParts[index] != formatPart) { + // We've found a format part that doesn't match. If this is the very last format part check + // for a string prefix match. If that doesn't match, we're done. + if (index == prefix.formatParts.size - 1 && formatParts[index].startsWith(formatPart)) { + firstFormatPart = formatParts[index].substring(formatPart.length) + } else { + return null + } + } + + // If the matching format part has an argument, check that too. + if (formatPart.startsWith("%") && !formatPart[1].isMultiCharNoArgPlaceholder) { + if (args[prefixArgCount] != prefix.args[prefixArgCount]) { + return null // Argument doesn't match. + } + prefixArgCount++ + } + } + + // We found a prefix. Prepare the suffix as a result. + val resultFormatParts = ArrayList() + firstFormatPart?.let { + resultFormatParts.add(it) + } + for (i in prefix.formatParts.size until formatParts.size) { + resultFormatParts.add(formatParts[i]) + } + + val resultArgs = ArrayList() + for (i in prefix.args.size until args.size) { + resultArgs.add(args[i]) + } + + return CodeBlock(resultFormatParts, resultArgs) + } + + /** + * Returns a copy of the code block without leading and trailing no-arg placeholders + * (`⇥`, `⇤`, `«`, `»`). + */ + internal fun trim(): CodeBlock { + var start = 0 + var end = formatParts.size + while (start < end && formatParts[start] in NO_ARG_PLACEHOLDERS) { + start++ + } + while (start < end && formatParts[end - 1] in NO_ARG_PLACEHOLDERS) { + end-- + } + return when { + start > 0 || end < formatParts.size -> CodeBlock(formatParts.subList(start, end), args) + else -> this + } + } + + /** + * Returns a copy of the code block with selected format parts replaced, similar to + * [java.lang.String.replaceAll]. + * + * **Warning!** This method leaves the arguments list unchanged. Take care when replacing + * placeholders with arguments, such as `%L`, as it can result in a code block, where + * placeholders don't match their arguments. + */ + internal fun replaceAll(oldValue: String, newValue: String) = + CodeBlock(formatParts.map { it.replace(oldValue, newValue) }, args) + + internal fun hasStatements() = formatParts.any { "«" in it } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + if (javaClass != other.javaClass) return false + return toString() == other.toString() + } + + override fun hashCode(): Int = toString().hashCode() + + override fun toString(): String = buildCodeString { emitCode(this@CodeBlock) } + + internal fun toString(codeWriter: CodeWriter): String = buildCodeString(codeWriter) { + emitCode(this@CodeBlock) + } + + fun toBuilder(): Builder { + val builder = Builder() + builder.formatParts += formatParts + builder.args.addAll(args) + return builder + } + + class Builder { + internal val formatParts = mutableListOf() + internal val args = mutableListOf() + + fun isEmpty(): Boolean = formatParts.isEmpty() + + fun isNotEmpty(): Boolean = !isEmpty() + + /** + * Adds code using named arguments. + * + * Named arguments specify their name after the '%' followed by : and the corresponding type + * character. Argument names consist of characters in `a-z, A-Z, 0-9, and _` and must start + * with a lowercase character. + * + * For example, to refer to the type [java.lang.Integer] with the argument name `clazz` use a + * format string containing `%clazz:T` and include the key `clazz` with value + * `java.lang.Integer.class` in the argument map. + */ + fun addNamed(format: String, arguments: Map): Builder = apply { + var p = 0 + + for (argument in arguments.keys) { + require(LOWERCASE matches argument) { + "argument '$argument' must start with a lowercase character" + } + } + + while (p < format.length) { + val nextP = format.nextPotentialPlaceholderPosition(startIndex = p) + if (nextP == -1) { + formatParts += format.substring(p, format.length) + break + } + + if (p != nextP) { + formatParts += format.substring(p, nextP) + p = nextP + } + + var matchResult: MatchResult? = null + val colon = format.indexOf(':', p) + if (colon != -1) { + val endIndex = (colon + 2).coerceAtMost(format.length) + matchResult = NAMED_ARGUMENT.matchEntire(format.substring(p, endIndex)) + } + if (matchResult != null) { + val argumentName = matchResult.groupValues[ARG_NAME] + require(arguments.containsKey(argumentName)) { + "Missing named argument for %$argumentName" + } + val formatChar = matchResult.groupValues[TYPE_NAME].first() + addArgument(format, formatChar, arguments[argumentName]) + formatParts += "%$formatChar" + p += matchResult.range.last + 1 + } else if (format[p].isSingleCharNoArgPlaceholder) { + formatParts += format.substring(p, p + 1) + p++ + } else { + require(p < format.length - 1) { "dangling % at end" } + require(format[p + 1].isMultiCharNoArgPlaceholder) { + "unknown format %${format[p + 1]} at ${p + 1} in '$format'" + } + formatParts += format.substring(p, p + 2) + p += 2 + } + } + } + + /** + * Add code with positional or relative arguments. + * + * Relative arguments map 1:1 with the placeholders in the format string. + * + * Positional arguments use an index after the placeholder to identify which argument index + * to use. For example, for a literal to reference the 3rd argument: "%3L" (1 based index) + * + * Mixing relative and positional arguments in a call to add is invalid and will result in an + * error. + */ + fun add(format: String, vararg args: Any?): Builder = apply { + var hasRelative = false + var hasIndexed = false + + var relativeParameterCount = 0 + val indexedParameterCount = IntArray(args.size) + + var p = 0 + while (p < format.length) { + if (format[p].isSingleCharNoArgPlaceholder) { + formatParts += format[p].toString() + p++ + continue + } + + if (format[p] != '%') { + var nextP = format.nextPotentialPlaceholderPosition(startIndex = p + 1) + if (nextP == -1) nextP = format.length + formatParts += format.substring(p, nextP) + p = nextP + continue + } + + p++ // '%'. + + // Consume zero or more digits, leaving 'c' as the first non-digit char after the '%'. + val indexStart = p + var c: Char + do { + require(p < format.length) { "dangling format characters in '$format'" } + c = format[p++] + } while (c in '0'..'9') + val indexEnd = p - 1 + + // If 'c' doesn't take an argument, we're done. + if (c.isMultiCharNoArgPlaceholder) { + require(indexStart == indexEnd) { "%% may not have an index" } + formatParts += "%$c" + continue + } + + // Find either the indexed argument, or the relative argument. (0-based). + val index: Int + if (indexStart < indexEnd) { + index = Integer.parseInt(format.substring(indexStart, indexEnd)) - 1 + hasIndexed = true + if (args.isNotEmpty()) { + indexedParameterCount[index % args.size]++ // modulo is needed, checked below anyway + } + } else { + index = relativeParameterCount + hasRelative = true + relativeParameterCount++ + } + + require(index >= 0 && index < args.size) { + "index ${index + 1} for '${ + format.substring( + indexStart - 1, + indexEnd + 1, + ) + }' not in range (received ${args.size} arguments)" + } + require(!hasIndexed || !hasRelative) { "cannot mix indexed and positional parameters" } + + addArgument(format, c, args[index]) + + formatParts += "%$c" + } + + if (hasRelative) { + require(relativeParameterCount >= args.size) { + "unused arguments: expected $relativeParameterCount, received ${args.size}" + } + } + if (hasIndexed) { + val unused = mutableListOf() + for (i in args.indices) { + if (indexedParameterCount[i] == 0) { + unused += "%" + (i + 1) + } + } + val s = if (unused.size == 1) EMPTY_STRING else "s" + require(unused.isEmpty()) { "unused argument$s: ${unused.joinToString(", ")}" } + } + } + + private fun addArgument(format: String, c: Char, arg: Any?) { + when (c) { + 'N' -> this.args += argToName(arg).escapeIfNecessary() + 'L' -> this.args += argToLiteral(arg) + 'S' -> this.args += argToString(arg) + 'P' -> this.args += if (arg is CodeBlock) arg else argToString(arg) + 'M' -> this.args += arg + 'C' -> this.args += argToString(arg) + 'T' -> this.args += argToType(arg) + else -> throw IllegalArgumentException( + String.format("invalid format string: '%s'", format), + ) + } + } + + private fun argToType(o: Any?) = when (o) { + is TypeName -> o + is Type -> o.asTypeName() + is KClass<*> -> o.asTypeName() + else -> throw IllegalArgumentException("expected type but was $o") + } + + private fun argToName(o: Any?) = when (o) { + is CharSequence -> o.toString() + /* is ParameterSpec -> o.name + is PropertySpec -> o.name + is FunSpec -> o.name + is TypeSpec -> o.name!! + is MemberName -> o.simpleName*/ + else -> throw IllegalArgumentException("expected name but was $o") + } + + private fun argToLiteral(o: Any?) = if (o is Number) formatNumericValue(o) else o + + private fun argToString(o: Any?) = o?.toString() + + private fun formatNumericValue(o: Number): Any? { + val format = DecimalFormatSymbols().apply { + decimalSeparator = '.' + groupingSeparator = '_' + } + + val precision = if (o is Float || o is Double) o.toString().split(".").last().length else 0 + + val pattern = when (o) { + is Float, is Double -> "###,##0.0" + "#".repeat(precision - 1) + else -> "###,##0" + } + + return DecimalFormat(pattern, format).format(o) + } + + /** + * @param controlFlow the control flow construct and its code, such as `if (foo == 5)`. + * Shouldn't contain newline characters. Can contain opening braces, e.g. + * `beginControlFlow("list.forEach { element ->")`. If there's no opening brace at the end + * of the string, it will be added. + */ + fun beginControlFlow(controlFlow: String, vararg args: Any?): Builder = apply { + add(controlFlow.withOpeningBrace(), *args) + indent() + } + + private fun String.withOpeningBrace(): String { + for (i in length - 1 downTo 0) { + if (this[i] == '{') { + return "$this\n" + } else if (this[i] == '}') { + break + } + } + return "$this·{\n" + } + + /** + * @param controlFlow the control flow construct and its code, such as "else if (foo == 10)". + * Shouldn't contain braces or newline characters. + */ + fun nextControlFlow(controlFlow: String, vararg args: Any?): Builder = apply { + unindent() + add("}·$controlFlow·{\n", *args) + indent() + } + + fun endControlFlow(): Builder = apply { + unindent() + add("}\n") + } + + fun addStatement(format: String, vararg args: Any?): Builder = apply { + add("«") + add(format, *args) + add("\n»") + } + + fun add(codeBlock: CodeBlock): Builder = apply { + formatParts += codeBlock.formatParts + args.addAll(codeBlock.args) + } + + fun indent(): Builder = apply { + formatParts += "⇥" + } + + fun unindent(): Builder = apply { + formatParts += "⇤" + } + + fun clear(): Builder = apply { + formatParts.clear() + args.clear() + } + + fun build(): CodeBlock = CodeBlock(formatParts.toImmutableList(), args.toImmutableList()) + } + + companion object { + private val NAMED_ARGUMENT = Regex("%([\\w_]+):([\\w]).*") + private val LOWERCASE = Regex("[a-z]+[\\w_]*") + private const val ARG_NAME = 1 + private const val TYPE_NAME = 2 + private val NO_ARG_PLACEHOLDERS = setOf("⇥", "⇤", "«", "»") + internal val EMPTY = CodeBlock(emptyList(), emptyList()) + + @JvmStatic + fun of(format: String, vararg args: Any?): CodeBlock = + Builder().add(format, *args).build() + + @JvmStatic + fun builder(): Builder = Builder() + + internal val Char.isMultiCharNoArgPlaceholder get() = this == '%' + internal val Char.isSingleCharNoArgPlaceholder get() = isOneOf('⇥', '⇤', '«', '»') + internal val String.isPlaceholder + get() = (length == 1 && first().isSingleCharNoArgPlaceholder) || + (length == 2 && first().isMultiCharNoArgPlaceholder) + + internal fun String.nextPotentialPlaceholderPosition(startIndex: Int) = + indexOfAny(charArrayOf('%', '«', '»', '⇥', '⇤'), startIndex) + } +} + +@JvmOverloads +fun Collection.joinToCode( + separator: CharSequence = ", ", + prefix: CharSequence = EMPTY_STRING, + suffix: CharSequence = EMPTY_STRING, +): CodeBlock { + val blocks = toTypedArray() + val placeholders = Array(blocks.size) { "%L" } + return CodeBlock.of(placeholders.joinToString(separator, prefix, suffix), *blocks) +} + +/** + * Builds new [CodeBlock] by populating newly created [CodeBlock.Builder] using provided + * [builderAction] and then converting it to [CodeBlock]. + */ +inline fun buildCodeBlock(builderAction: CodeBlock.Builder.() -> Unit): CodeBlock { + return CodeBlock.builder().apply(builderAction).build() +} + +/** + * Calls [CodeBlock.Builder.indent] then executes the provided [builderAction] on the + * [CodeBlock.Builder] and then executes [CodeBlock.Builder.unindent] before returning the + * original [CodeBlock.Builder]. + */ +inline fun CodeBlock.Builder.withIndent(builderAction: CodeBlock.Builder.() -> Unit): CodeBlock.Builder { + return indent().also(builderAction).unindent() +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/CodeFragment.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/CodeFragment.kt deleted file mode 100644 index 4d2ae4eb..00000000 --- a/src/main/kotlin/net/theevilreaper/dartpoet/code/CodeFragment.kt +++ /dev/null @@ -1,16 +0,0 @@ -package net.theevilreaper.dartpoet.code - -class CodeFragment( - private var formatParts: List, - private var args: List -) { - - fun isEmpty() = formatParts.isEmpty() - - fun toBuilder(): CodeFragmentBuilder { - val builder = CodeFragmentBuilder() - builder.formatParts += formatParts - builder.args.addAll(args) - return builder - } -} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/CodeFragmentBuilder.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/CodeFragmentBuilder.kt deleted file mode 100644 index 708ece15..00000000 --- a/src/main/kotlin/net/theevilreaper/dartpoet/code/CodeFragmentBuilder.kt +++ /dev/null @@ -1,194 +0,0 @@ -package net.theevilreaper.dartpoet.code - -import net.theevilreaper.dartpoet.util.toImmutableList -import net.theevilreaper.dartpoet.writer.FragmentPart -import java.text.DecimalFormat -import java.text.DecimalFormatSymbols - -/** - * codeblock.add("this.add(%$1T);", object); - * - */ - -private val NO_ARG_PLACEHOLDERS = arrayOf('⇥', '⇤', '«', '»').toCharArray() - -class CodeFragmentBuilder( - internal val formatParts: MutableList = mutableListOf(), // 0 - internal val args: MutableList = mutableListOf(), // 0 -) { - - companion object { - @JvmStatic - fun of(format: String, vararg args: Any?): CodeFragment = builder().add(format, args).build() - - @JvmStatic - fun builder(): CodeFragmentBuilder = CodeFragmentBuilder() - internal val Char.isSingleCharNoArgPlaceholder get() = this in NO_ARG_PLACEHOLDERS - internal val Char.isMultiCharNoArgPlaceholder get() = this == '%' - - internal fun String.nextPotentialPlaceholderPosition(startIndex: Int) = - indexOfAny(NO_ARG_PLACEHOLDERS, startIndex) - } - - fun add(format: String, vararg args: Any?): CodeFragmentBuilder = apply { - if (format.trim().isEmpty()) return@apply - - var hasRelative = false - var hasIndexed = false - - var relativeParameterCount = 0 - val indexedParameterCount = IntArray(args.size) - - var counter = 0 - - while (counter < format.length) { - // Checks if the char is a placeholder - if (format[counter].isSingleCharNoArgPlaceholder) { - formatParts += format[counter].toString() - counter++ - continue - } - - if (format[counter] != '%') { - var nextPlaceholder = format.nextPotentialPlaceholderPosition(startIndex = counter + 1) - if (nextPlaceholder == -1) nextPlaceholder = format.length - formatParts += format.substring(counter, nextPlaceholder) - counter = nextPlaceholder - continue - } - - counter++ - - // Consume zero or more digits, leaving 'c' as the first non-digit char after the '%'. - val indexStart = counter - var c: Char - do { - require(counter < format.length) { "dangling format characters in '$format'" } - c = format[counter++] - } while (c in '0'..'9') - val indexEnd = counter - 1 - - // If 'c' doesn't take an argument, we're done. - if (c.isMultiCharNoArgPlaceholder) { - require(indexStart == indexEnd) { "%% may not have an index" } - formatParts += "%$c" - continue - } - - val index: Int - if (indexStart < indexEnd) { - index = Integer.parseInt(format.substring(indexStart, indexEnd)) - 1 - hasIndexed = true - if (args.isNotEmpty()) { - indexedParameterCount[index % args.size]++ // modulo is needed, checked below anyway - } - } else { - index = relativeParameterCount - hasRelative = true - relativeParameterCount++ - } - - require(index >= 0 && index < args.size) { - "index ${index + 1} for '${format.substring( - indexStart - 1, - indexEnd + 1, - )}' not in range (received ${args.size} arguments)" - } - require(!hasIndexed || !hasRelative) { "cannot mix indexed and positional parameters" } - - addArgument(format, c, args[index]) - - formatParts += "%$c" - - } - - if (hasRelative) { - require(relativeParameterCount >= args.size) { - "unused arguments: expected $relativeParameterCount, received ${args.size}" - } - } - if (hasIndexed) { - val unused = mutableListOf() - for (i in args.indices) { - if (indexedParameterCount[i] == 0) { - unused += "%" + (i + 1) - } - } - val s = if (unused.size == 1) "" else "s" - require(unused.isEmpty()) { "unused argument$s: ${unused.joinToString(", ")}" } - } - } - - private fun addArgument(format: String, c: Char, arg: Any?) { - val part: FragmentPart = FragmentPart.mapByIdentifier(c) - ?: throw IllegalArgumentException(String.format("invalid format string: '%s'", format)) - - when (part) { - FragmentPart.NAMED -> this.args += argToName(arg) - FragmentPart.LITERAL -> this.args += argToLiteral(arg) - FragmentPart.STRING -> this.args += argToString(arg) - FragmentPart.STRING_NOT_ESCAPED -> this.args += if (arg is CodeFragment) arg else argToString(arg) - FragmentPart.MEMBER -> this.args += arg - } - } - - private fun argToName(o: Any?) = when (o) { - is CharSequence -> o.toString() - /* is ParameterSpec -> o.name - is PropertySpec -> o.name - is FunSpec -> o.name - is TypeSpec -> o.name!! - is MemberName -> o.simpleName*/ - else -> throw IllegalArgumentException("expected name but was $o") - } - - private fun argToLiteral(o: Any?) = if (o is Number) formatNumericValue(o) else o - - private fun argToString(o: Any?) = o?.toString() - - private fun formatNumericValue(o: Number): Any? { - val format = DecimalFormatSymbols().apply { - decimalSeparator = '.' - groupingSeparator = '_' - } - - val precision = if (o is Float || o is Double) o.toString().split(".").last().length else 0 - - val pattern = when (o) { - is Float, is Double -> "###,##0.0" + "#".repeat(precision - 1) - else -> "###,##0" - } - - return DecimalFormat(pattern, format).format(o) - } - - /*private fun argToType(o: Any?) = when (o) { - *//*is TypeName -> o - is TypeMirror -> { - logDeprecationWarning(o) - o.asTypeName() - } - is Element -> { - logDeprecationWarning(o) - o.asType().asTypeName() - } - is Type -> o.asTypeName() - is KClass<*> -> o.asTypeName()*//* - else -> throw IllegalArgumentException("expected type but was $o") - }*/ - - fun indent(): CodeFragmentBuilder = apply { - formatParts += "⇥" - } - - fun unindent(): CodeFragmentBuilder = apply { - formatParts += "⇤" - } - - fun clear(): CodeFragmentBuilder = apply { - formatParts.clear() - args.clear() - } - - fun build(): CodeFragment = CodeFragment(formatParts.toImmutableList(), args.toImmutableList()) -} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/CodeWriter.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/CodeWriter.kt new file mode 100644 index 00000000..e087eb26 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/code/CodeWriter.kt @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2015 Square, 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 + * + * https://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. + * + * Changes to the file compared to the original: + * + * The file contains only this methods which are needed to write the code for Dart. + * All main method which writes the code in KotlinPoet still exists in this writer adaption. + * Some others has been removed because they are not required + */ +package net.theevilreaper.dartpoet.code + +import net.theevilreaper.dartpoet.type.TypeName +import net.theevilreaper.dartpoet.util.* +import net.theevilreaper.dartpoet.util.NEW_LINE +import net.theevilreaper.dartpoet.util.escapeCharacterLiterals +import net.theevilreaper.dartpoet.util.stringLiteralWithQuotes +import java.io.Closeable + +internal inline fun buildCodeString(builderAction: CodeWriter.() -> Unit): String { + val stringBuilder = StringBuilder() + CodeWriter(stringBuilder, columnLimit = Integer.MAX_VALUE).use { + it.builderAction() + } + return stringBuilder.toString() +} + +internal fun buildCodeString( + codeWriter: CodeWriter, + builderAction: CodeWriter.() -> Unit, +): String { + val stringBuilder = StringBuilder() + codeWriter.emitInto(stringBuilder, builderAction) + return stringBuilder.toString() +} + +/** + * Converts a [FileSpec] to a string suitable to both human- and kotlinc-consumption. This honors + * imports, indentation, and deferred variable names. + */ +class CodeWriter constructor( + out: Appendable, + private val indent: String = DEFAULT_INDENT, + columnLimit: Int = 100, +) : Closeable { + var out = LineWrapper(out, indent, columnLimit) + private var indentLevel = 0 + private var comment = false + private var trailingNewline = false + + /** + * When emitting a statement, this is the line of the statement currently being written. The first + * line of a statement is indented normally and subsequent wrapped lines are double-indented. This + * is -1 when the currently-written line isn't part of a statement. + */ + private var statementLine = -1 + + fun indent(levels: Int = 1) = apply { + indentLevel += levels + } + + fun unindent(levels: Int = 1) = apply { + require(indentLevel - levels >= 0) { "cannot unindent $levels from $indentLevel" } + indentLevel -= levels + } + + fun emitDoc(codeBlock: CodeBlock) { + trailingNewline = true // Force the '///' prefix for the documentation. + comment = true + try { + emitCode(codeBlock) + emit(NEW_LINE) + } finally { + comment = false + } + } + + fun emitCode(s: String) = emitCode(CodeBlock.of(s)) + + fun emitCode(format: String, vararg args: Any?) = emitCode(CodeBlock.of(format, *args)) + + fun emitCode( + codeBlock: CodeBlock, + isConstantContext: Boolean = false, + ensureTrailingNewline: Boolean = false, + ) = apply { + var a = 0 + val partIterator = codeBlock.formatParts.listIterator() + while (partIterator.hasNext()) { + when (val part = partIterator.next()) { + "%L" -> emitLiteral(codeBlock.args[a++], isConstantContext) + "%S", "%C" -> { + val string = codeBlock.args[a++] as String? + // Emit null as a literal null: no quotes. + val literal = if (string != null) { + stringLiteralWithQuotes( + string, + isInsideRawString = false, + isConstantContext = isConstantContext, + ) + } else { + NULL_STRING + } + + if (part == "%C") { + emit(literal.replace("\"", "'"), nonWrapping = true) + } else { + emit(literal, nonWrapping = true) + } + } + "%P" -> { + val string = codeBlock.args[a++]?.let { arg -> + if (arg is CodeBlock) { + arg.toString(this@CodeWriter) + } else { + arg as String? + } + } + // Emit null as a literal null: no quotes. + val literal = if (string != null) { + stringLiteralWithQuotes( + string, + isInsideRawString = true, + isConstantContext = isConstantContext, + ) + } else { + NULL_STRING + } + emit(literal.replace("\"", "'"), nonWrapping = true) + } + "%T" -> { + var typeName = codeBlock.args[a++] as TypeName + typeName.emit(this) + } + "%%" -> emit("%") + "⇥" -> indent() + "⇤" -> unindent() + "«" -> { + check(statementLine == -1) { + """ + |Can't open a new statement until the current statement is closed (opening « followed + |by another « without a closing »). + |Current code block: + |- Format parts: ${codeBlock.formatParts.map(::escapeCharacterLiterals)} + |- Arguments: ${codeBlock.args} + | + """.trimMargin() + } + statementLine = 0 + } + + "»" -> { + check(statementLine != -1) { + """ + |Can't close a statement that hasn't been opened (closing » is not preceded by an + |opening «). + |Current code block: + |- Format parts: ${codeBlock.formatParts.map(::escapeCharacterLiterals)} + |- Arguments: ${codeBlock.args} + | + """.trimMargin() + } + if (statementLine > 0) { + unindent(2) // End a multi-line statement. Decrease the indentation level. + } + statementLine = -1 + } + else -> { + emit(part) + } + } + } + if (ensureTrailingNewline && out.hasPendingSegments) { + emit(NEW_LINE) + } + } + + private fun emitLiteral(o: Any?, isConstantContext: Boolean) { + when (o) { + /*is TypeSpec -> o.emit(this, null) + is AnnotationSpec -> o.emit(this, inline = true, asParameter = isConstantContext) + is PropertySpec -> o.emit(this, emptySet()) + is FunSpec -> o.emit( + codeWriter = this, + enclosingName = null, + implicitModifiers = setOf(KModifier.PUBLIC), + includeKdocTags = true, + ) + is TypeAliasSpec -> o.emit(this)*/ + is CodeBlock -> emitCode(o, isConstantContext = isConstantContext) + else -> emit(o.toString()) + } + } + + /** + * Emits `s` with indentation as required. It's important that all code that writes to + * [CodeWriter.out] does it through here, since we emit indentation lazily in order to avoid + * unnecessary trailing whitespace. + */ + fun emit(s: String, nonWrapping: Boolean = false) = apply { + var first = true + for (line in s.split('\n')) { + // Emit a newline character. Make sure blank lines in KDoc & comments look good. + if (!first) { + if (comment && trailingNewline) { + emitIndentation() + out.appendNonWrapping(DOCUMENTATION_CHAR) + } + out.newline() + trailingNewline = true + if (statementLine != -1) { + if (statementLine == 0) { + indent(2) // Begin multiple-line statement. Increase the indentation level. + } + statementLine++ + } + } + + first = false + if (line.isEmpty()) continue // Don't indent empty lines. + + // Emit indentation and comment prefix if necessary. + if (trailingNewline) { + emitIndentation() + if (comment) { + // To get insides why we are writing /// for documentation + // Please take a look at this side https://dart.dev/effective-dart/documentation + out.appendNonWrapping("$DOCUMENTATION_CHAR ") + + } + } + + if (nonWrapping) { + out.appendNonWrapping(line) + } else { + out.append( + line, + indentLevel = indentLevel + 2, + ) + } + trailingNewline = false + } + } + + private fun emitIndentation() { + for (j in 0 until indentLevel) { + out.appendNonWrapping(indent) + } + } + + /** + * Perform emitting actions on the current [CodeWriter] using a custom [Appendable]. The + * [CodeWriter] will continue using the old [Appendable] after this method returns. + */ + inline fun emitInto(out: Appendable, action: CodeWriter.() -> Unit) { + val codeWrapper = this + LineWrapper(out, indent = DEFAULT_INDENT, maxLineLength = Int.MAX_VALUE).use { newOut -> + val oldOut = codeWrapper.out + codeWrapper.out = newOut + action() + codeWrapper.out = oldOut + } + } + + /** + * Closes the underlying [Appendable]. + */ + override fun close() { + out.close() + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/CodeWriterExtension.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/CodeWriterExtension.kt new file mode 100644 index 00000000..621852ea --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/code/CodeWriterExtension.kt @@ -0,0 +1,226 @@ +package net.theevilreaper.dartpoet.code + +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import net.theevilreaper.dartpoet.extension.ExtensionSpec +import net.theevilreaper.dartpoet.function.FunctionSpec +import net.theevilreaper.dartpoet.function.constructor.ConstructorSpec +import net.theevilreaper.dartpoet.directive.Directive +import net.theevilreaper.dartpoet.function.typedef.TypeDefSpec +import net.theevilreaper.dartpoet.property.consts.ConstantPropertySpec +import net.theevilreaper.dartpoet.parameter.ParameterSpec +import net.theevilreaper.dartpoet.property.PropertySpec +import net.theevilreaper.dartpoet.util.CURLY_CLOSE +import net.theevilreaper.dartpoet.util.CURLY_OPEN +import net.theevilreaper.dartpoet.util.EMPTY_STRING +import net.theevilreaper.dartpoet.util.NEW_LINE +import net.theevilreaper.dartpoet.util.SPACE + +internal val NO_ARG_PLACEHOLDERS = arrayOf('%', '⇥', '⇤', '«', '»').toCharArray() +internal val NO_ARG_PLACEHOLDERS_STRING = setOf("⇥", "⇤", "«", "»") +internal val SPECIAL_CHARACTERS = " \n·".toCharArray() +internal val UNSAFE_LINE_START = Regex("\\s*[-+].*") + +fun String.withOpenBrackets(): String { + + for (i in length - 1 downTo 0) { + if (this[i] == CURLY_OPEN) { + return "$this$NEW_LINE" + } else if (this[i] == CURLY_CLOSE) { + break + } + } + return "$this $CURLY_OPEN" +} + +val Char.isSingleCharNoArgPlaceholder get() = this in NO_ARG_PLACEHOLDERS +val Char.isMultiCharNoArgPlaceholder get() = this == '%' + + +internal val String.isPlaceholder + get() = (length == 1 && first().isSingleCharNoArgPlaceholder) || + (length == 2 && first().isMultiCharNoArgPlaceholder) +fun String.nextPotentialPlaceholderPosition(startIndex: Int) = + indexOfAny(NO_ARG_PLACEHOLDERS, startIndex) + +internal fun Set.emitFunctions( + codeWriter: CodeWriter, + emitBlock: (FunctionSpec) -> Unit = { it.write(codeWriter) } +) = with(codeWriter) { + if (isNotEmpty()) { + val newLines = size > 1 + forEachIndexed { index, functionSpec -> + if (index > 0) { + emit(NEW_LINE) + } + emitBlock(functionSpec) + if (newLines && index < size - 1) { + emit(NEW_LINE) + } + } + } +} + +internal fun Set.emitAnnotations( + codeWriter: CodeWriter, + inLineAnnotations: Boolean = true, + endWithNewLine: Boolean = true, + emitBlock: (AnnotationSpec) -> Unit = { it.write(codeWriter) } +) = with(codeWriter) { + if (isNotEmpty()) { + forEachIndexed { index, annotation -> + if (index > 0) { + codeWriter.emit(if (inLineAnnotations) EMPTY_STRING else NEW_LINE) + } + emitBlock(annotation) + } + + if (endWithNewLine) { + emit(NEW_LINE) + } else { + emit(SPACE) + } + } +} + +internal fun Set.emitConstructors( + codeWriter: CodeWriter, + forceNewLines: Boolean = false, + leadingNewLine: Boolean = false, + emitBlock: (ConstructorSpec) -> Unit = { it.write(codeWriter) } +) = with(codeWriter) { + if (isNotEmpty()) { + if (leadingNewLine) { + codeWriter.emit(NEW_LINE) + } + forEachIndexed { index, constructorSpec -> + val emitNewLines = size >= 1 || forceNewLines + + if (index > 0 && emitNewLines) { + codeWriter.emit(NEW_LINE) + } + + emitBlock(constructorSpec) + + if (emitNewLines) { + codeWriter.emit(NEW_LINE) + } + } + } +} + +internal fun List.emitParameters( + codeWriter: CodeWriter, + forceNewLines: Boolean = false, + emitSpace: Boolean = true, + emitBlock: (ParameterSpec) -> Unit = { it.write(codeWriter) } +) = with(codeWriter) { + if (isNotEmpty()) { + val emitComma = size > 1 + forEachIndexed { index, parameter -> + if (index > 0 && forceNewLines) { + emit(NEW_LINE) + } + + emitBlock(parameter) + if (emitComma) { + if (index < size - 1) { + emit(",") + } + if (emitSpace && index < size - 1) { + emit(SPACE) + } + } + } + } +} + +internal fun List.emitExtensions( + codeWriter: CodeWriter, + forceNewLines: Boolean = false, + emitBlock: (ExtensionSpec) -> Unit = { it.write(codeWriter) } +) = with(codeWriter) { + if (isNotEmpty()) { + val emitNewLines = size > 1 || forceNewLines + + forEachIndexed { index, parameter -> + if (index > 0 && emitNewLines) { + emit(NEW_LINE) + } + emitBlock(parameter) + } + + if (emitNewLines) { + codeWriter.emit(NEW_LINE) + } + } +} + +internal fun List.writeImports( + writer: CodeWriter, + newLineAtBegin: Boolean = true, + emitBlock: (T) -> String = { it.asString() } +) { + if (isNotEmpty()) { + if (newLineAtBegin) { + writer.emit(NEW_LINE) + } + forEachIndexed { index, import -> + if (index > 0) { + writer.emit(NEW_LINE) + } + + writer.emit(emitBlock(import)) + } + + writer.emit(NEW_LINE) + } +} + +fun Set.emitConstants( + codeWriter: CodeWriter, + emitBlock: (ConstantPropertySpec) -> Unit = { it.write(codeWriter) } +) = with(codeWriter) { + if (isNotEmpty()) { + val emitNewLines = size > 1 + + forEachIndexed { index, property -> + if (index > 0) { + emit(if (emitNewLines) NEW_LINE else EMPTY_STRING) + } + emitBlock(property) + } + emit(NEW_LINE) + } +} + +fun List.emitTypeDefs( + codeWriter: CodeWriter, + emitBlock: (TypeDefSpec) -> Unit = { it.write(codeWriter) } +) = with(codeWriter) { + val emitNewLines = size > 1 + forEachIndexed { index, typeDefSpec -> + if (index > 0 && emitNewLines) { + emit(NEW_LINE) + } + emitBlock(typeDefSpec) + } + emit(NEW_LINE) +} + +fun Set.emitProperties( + codeWriter: CodeWriter, + forceNewLines: Boolean = false, + emitBlock: (PropertySpec) -> Unit = { it.write(codeWriter) } +) = with(codeWriter) { + if (isNotEmpty()) { + val emitNewLines = size > 1 || forceNewLines + + forEachIndexed { index, property -> + if (index > 0) { + emit(if (emitNewLines) NEW_LINE else EMPTY_STRING) + } + emitBlock(property) + } + emit(NEW_LINE) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/DocumentationAppender.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/DocumentationAppender.kt new file mode 100644 index 00000000..4823b427 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/code/DocumentationAppender.kt @@ -0,0 +1,19 @@ +package net.theevilreaper.dartpoet.code + +/** + * This interface contains a default implementation to append a documentation from a spec to a [CodeWriter] instance. + * @author theEvilReaper + * @since 1.0.0 + */ +internal interface DocumentationAppender { + + /** + * When a spec contains any kind of documentation this method will emit it into a [CodeWriter]. + * @param docs the list of documentation + * @param writer the [CodeWriter] instance to write the code + */ + fun emitDocumentation(docs: List, writer: CodeWriter) { + if (docs.isEmpty()) return + docs.forEach { writer.emitDoc(it) } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/InitializerAppender.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/InitializerAppender.kt new file mode 100644 index 00000000..f6e1af07 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/code/InitializerAppender.kt @@ -0,0 +1,35 @@ +package net.theevilreaper.dartpoet.code + +/** + * An internal interface for appending initializer blocks to a [CodeWriter] for a given type [T]. + * Initializer blocks contain data that should be emitted into a [CodeWriter] instance. + * This interface provides methods to write initializer blocks for a [CodeBlock] or an object of type [T]. + * + * @param T the type of object for which initializer blocks are appended. + * @author theEvilReaper + * @since 1.0.0 + */ +internal interface InitializerAppender { + + /** + * When a spec object contains data for an initializer block it should be emitted into a writer instance. + * @param initBlock the [CodeBlock] which contains initializer the data + * @param writer the [CodeWriter] instance to write the code + */ + fun writeInitBlock(initBlock: CodeBlock, writer: CodeWriter, isConstantContext: Boolean = true) { + if (initBlock.isEmpty()) return + writer.emit("·=·") + writer.emitCode(initBlock, isConstantContext) + } + + /** + * When a spec object contains data for an initializer block it should be emitted into a writer instance. + * @param spec the spec object which contains the initializer block + * @param writer the [CodeWriter] instance to write the code + * @param isConstantContext a flag indicating whether the context is constant (default is true). + * @throws UnsupportedOperationException if this method is called and not implemented. + */ + fun writeInitBlock(spec: T, writer: CodeWriter, isConstantContext: Boolean = true) { + throw UnsupportedOperationException("Not implemented yet") + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/LineWrapper.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/LineWrapper.kt new file mode 100644 index 00000000..87289bb3 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/code/LineWrapper.kt @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2016 Square, 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 + * + * https://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. + * + * https://github.com/square/kotlinpoet/blob/master/kotlinpoet/src/main/java/com/squareup/kotlinpoet/LineWrapper.kt + * + * Additional changes: + * - Outsource clear logic into a own method + * - Use constant values for \n and other chars + */ +package net.theevilreaper.dartpoet.code + +import net.theevilreaper.dartpoet.util.EMPTY_STRING +import net.theevilreaper.dartpoet.util.NEW_LINE +import net.theevilreaper.dartpoet.util.NEW_LINE_CHAR +import net.theevilreaper.dartpoet.util.SPACE +import net.theevilreaper.dartpoet.util.SPACE_CHAR +import java.io.Closeable + +/** + * The class is the LineWrapper from kotlinpoet (see header from the class) with some additional changes. + */ +class LineWrapper( + private val out: Appendable, + private val indent: String, + private val maxLineLength: Int +) : Closeable { + + private var closed = false + + /** + * Segments of the current line to be joined by spaces or wraps. Never empty, but contains a lone + * empty string if no data has been emitted since the last newline. + */ + private val segments = mutableListOf(EMPTY_STRING) + + /** Number of indents in wraps. -1 if the current line has no wraps. */ + private var indentLevel = -1 + + /** Optional prefix that will be prepended to wrapped lines. */ + private var linePrefix = EMPTY_STRING + + /** @return whether there are pending segments for the current line. */ + val hasPendingSegments get() = segments.size != 1 || segments[0].isNotEmpty() + + /** Emit `s` replacing its spaces with line wraps as necessary. */ + fun append(string: String, indentLevel: Int = -1, linePrefix: String = EMPTY_STRING) { + check(!closed) { "closed" } + + var pos = 0 + while (pos < string.length) { + when (string[pos]) { + SPACE_CHAR -> { + // Each space starts a new empty segment. + this.indentLevel = indentLevel + this.linePrefix = linePrefix + segments += EMPTY_STRING + pos++ + } + + NEW_LINE_CHAR -> { + // Each newline emits the current segments. + newline() + pos++ + } + + '·' -> { + // Render · as a non-breaking space. + segments[segments.size - 1] += SPACE + pos++ + } + + else -> { + var next = string.indexOfAny(SPECIAL_CHARACTERS, pos) + if (next == -1) next = string.length + segments[segments.size - 1] += string.substring(pos, next) + pos = next + } + } + } + } + + /** Emit `s` leaving spaces as-is. */ + fun appendNonWrapping(s: String) { + check(!closed) { "closed" } + require(!s.contains(NEW_LINE)) + segments[segments.size - 1] += s + } + + fun newline() { + check(!closed) { "closed" } + emitCurrentLine() + out.appendLine() + indentLevel = -1 + } + + /** Flush any outstanding text and forbid future writes to this line wrapper. */ + override fun close() { + emitCurrentLine() + closed = true + } + + /** + * Writes the current line into the given [Appendable]. + */ + private fun emitCurrentLine() { + foldUnsafeBreaks() + + var start = 0 + var columnCount = segments[0].length + + for (i in 1 until segments.size) { + val segment = segments[i] + val newColumnCount = columnCount + 1 + segment.length + + // If this segment doesn't fit in the current run, print the current run and start a new one. + if (newColumnCount > maxLineLength) { + emitSegmentRange(start, i) + start = i + columnCount = segment.length + indent.length * indentLevel + continue + } + + columnCount = newColumnCount + } + + // Print the last run. + emitSegmentRange(start, segments.size) + clear() + } + + private fun clear() { + segments.clear() + segments += EMPTY_STRING + } + + private fun emitSegmentRange(startIndex: Int, endIndex: Int) { + // If this is a wrapped line we need a newline and an indent. + if (startIndex > 0) { + out.appendLine() + for (i in 0 until indentLevel) { + out.append(indent) + } + out.append(linePrefix) + } + + // Emit each segment separated by spaces. + out.append(segments[startIndex]) + for (i in startIndex + 1 until endIndex) { + out.append(SPACE) + out.append(segments[i]) + } + } + + /** + * Any segment that starts with '+' or '-' can't have a break preceding it. Combine it with the + * preceding segment. Note that this doesn't apply to the first segment. + */ + private fun foldUnsafeBreaks() { + var i = 1 + while (i < segments.size) { + val segment = segments[i] + if (UNSAFE_LINE_START.matches(segment)) { + segments[i - 1] = segments[i - 1] + SPACE + segments[i] + segments.removeAt(i) + if (i > 1) i-- + } else { + i++ + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/Writeable.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/Writeable.kt new file mode 100644 index 00000000..674b59ee --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/code/Writeable.kt @@ -0,0 +1,16 @@ +package net.theevilreaper.dartpoet.code + +/** + * Represents a class that can emit a spec structure to a [CodeWriter]. + * @author theEvilReaper + * @since 1.0.0 + */ +fun interface Writeable { + + /** + * Emits the content from the generic [T] object to a [CodeWriter] instance. + * @param spec the spec to emit + * @param writer the writer to write the spec to + */ + fun write(spec: T, writer: CodeWriter) +} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/AnnotationWriter.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/AnnotationWriter.kt new file mode 100644 index 00000000..2ed8a41e --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/AnnotationWriter.kt @@ -0,0 +1,65 @@ +package net.theevilreaper.dartpoet.code.writer + +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import net.theevilreaper.dartpoet.code.CodeBlock +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.joinToCode +import net.theevilreaper.dartpoet.util.ANNOTATION_CHAR +import net.theevilreaper.dartpoet.util.EMPTY_STRING +import net.theevilreaper.dartpoet.util.NEW_LINE +import net.theevilreaper.dartpoet.util.ROUND_CLOSE +import net.theevilreaper.dartpoet.util.ROUND_OPEN +import net.theevilreaper.dartpoet.util.toImmutableList + +/** + * The [AnnotationWriter] is responsible for writing the data from an [AnnotationSpec] into valid code for dart code. + * @since 1.0.0 + * @author theEvilReaper + */ +internal class AnnotationWriter { + + /** + * Writes the data which are stored in a [AnnotationSpec] to a given instance from a [CodeWriter]. + * @param spec the spec to write + * @param writer the writer to write the spec to + * @param inline if the spec should be written inline + */ + fun emit(spec: AnnotationSpec, writer: CodeWriter, inline: Boolean) { + writer.emit(ANNOTATION_CHAR) + writer.emitCode("%T", spec.typeName) + + if (!spec.hasContent) return + + val whitespace = if (inline) EMPTY_STRING else NEW_LINE + val memberSeparator = if (inline) ", " else ",\n" + val memberSuffix = if (!inline && spec.content.size > 1) "," else EMPTY_STRING + + writer.emit(ROUND_OPEN) + if (spec.hasMultipleContentParts) writer.emit(whitespace).indent() + writer.emitCode( + codeBlock = mapCodeBlocks(spec, inline, memberSeparator, memberSuffix), + isConstantContext = true, + ) + if (spec.hasMultipleContentParts) writer.unindent().emit(whitespace) + writer.emit(ROUND_CLOSE) + } + + /** + * Maps the content from an [AnnotationSpec] into a [CodeBlock] reference. + * @param spec the spec to map + * @param inline if the spec should be written inline + * @param memberSeparator the separator for the members + * @param memberSuffix the suffix for the members + * @return the created [CodeBlock] reference + */ + private fun mapCodeBlocks( + spec: AnnotationSpec, + inline: Boolean, + memberSeparator: String, + memberSuffix: String, + ): CodeBlock { + return spec.content.toImmutableList() + .map { if (inline) it.replaceAll("[⇥|⇤]", EMPTY_STRING) else it } + .joinToCode(separator = memberSeparator, suffix = memberSuffix) + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/ClassWriter.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/ClassWriter.kt new file mode 100644 index 00000000..81dac761 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/ClassWriter.kt @@ -0,0 +1,158 @@ +package net.theevilreaper.dartpoet.code.writer + +import net.theevilreaper.dartpoet.DartModifier.* +import net.theevilreaper.dartpoet.clazz.ClassType +import net.theevilreaper.dartpoet.clazz.ClassSpec +import net.theevilreaper.dartpoet.code.* +import net.theevilreaper.dartpoet.code.emitAnnotations +import net.theevilreaper.dartpoet.code.emitConstructors +import net.theevilreaper.dartpoet.code.emitFunctions +import net.theevilreaper.dartpoet.enum.EnumPropertySpec +import net.theevilreaper.dartpoet.util.CURLY_CLOSE +import net.theevilreaper.dartpoet.util.CURLY_OPEN +import net.theevilreaper.dartpoet.util.EMPTY_STRING +import net.theevilreaper.dartpoet.util.NEW_LINE +import net.theevilreaper.dartpoet.util.SEMICOLON + +/** + * @version 1.0.0 + * @since 1.0.0 + * @author theEvilReaper + */ +internal class ClassWriter : Writeable { + + //TODO: Improve new lines after each generated code part block + override fun write(spec: ClassSpec, writer: CodeWriter) { + if (spec.isAnonymous) { + writeAnonymousClass(spec, writer) + return + } + + spec.annotations.emitAnnotations(writer, inLineAnnotations = false) + writeClassHeader(spec, writer) + + //Only write {} when the class contains now content + if (spec.hasNoContent) { + writer.emit("$CURLY_OPEN$CURLY_CLOSE") + + if (spec.endsWithNewLine) { + writer.emit(NEW_LINE) + } + + return + } + + writeInheritance(spec, writer) + + writer.emit("{$NEW_LINE") + writer.emit(NEW_LINE) + writer.indent() + + if (spec.isEnum) { + spec.enumPropertyStack.emit(writer) { + it.write(writer) + } + + if (!spec.hasNoContent) { + writer.emit(SEMICOLON) + writer.emit(NEW_LINE) + } + } + + if (spec.isEnum && spec.enumPropertyStack.isNotEmpty()) { + writer.emit(NEW_LINE) + } + + spec.constantStack.emitConstants(writer) + + if (spec.constantStack.isNotEmpty()) { + writer.emit(NEW_LINE) + } + + spec.properties.emitProperties(writer) + + if (spec.properties.isNotEmpty()) { + writer.emit(NEW_LINE) + } + + spec.constructors.emitConstructors(writer) + + if (spec.constructors.isNotEmpty() && spec.constructors.size <= 1) { + writer.emit(NEW_LINE) + } + + spec.functions.emitFunctions(writer) + + writer.unindent() + if (spec.functions.isNotEmpty()) { + writer.emit(NEW_LINE) + } + writer.emit("}") + + if (spec.endsWithNewLine) { + writer.emit(NEW_LINE) + } + } + + private fun writeAnonymousClass(spec: ClassSpec, writer: CodeWriter) { + spec.typeDefs.emitTypeDefs(writer) + spec.functions.emitFunctions(writer) + + if (spec.endsWithNewLine) { + writer.emit(NEW_LINE) + } + + } + + /** + * The method contains the logic to write the dart class declaration for a [ClassSpec]. + * @param spec the [ClassSpec] which contains all data for a class + * @param writer the [CodeWriter] to write the class declaration + */ + private fun writeClassHeader(spec: ClassSpec, writer: CodeWriter) { + when (val type = spec.classType) { + ClassType.CLASS, ClassType.MIXIN, ClassType.ENUM -> { + writer.emit(type.keyword) + writer.emit("·") + writer.emit(if (spec.modifiers.contains(PRIVATE)) PRIVATE.identifier else EMPTY_STRING) + if (spec.name.orEmpty().trim().isNotEmpty()) { + writer.emit(spec.name!!) + } + } + + ClassType.ABSTRACT -> { + writer.emit("${type.keyword}·${ClassType.CLASS.keyword}·") + writer.emit(if (spec.modifiers.contains(PRIVATE)) PRIVATE.identifier else EMPTY_STRING) + writer.emit(spec.name!!) + } + + else -> { + //TODO: Check if a library class needs a header + // A library class doesn't have any class header + } + } + writer.emit("·") + } + + private fun writeInheritance(spec: ClassSpec, writer: CodeWriter) { + if (spec.superClass != null) { + writer.emit("${spec.inheritKeyWord!!.identifier}·") + writer.emitCode("%T", spec.superClass) + writer.emit("·") + } + } + + private fun List.emit( + codeWriter: CodeWriter, + emitBlock: (EnumPropertySpec) -> Unit = { it.write(codeWriter) } + ) = with(codeWriter) { + if (isNotEmpty()) { + forEachIndexed { index, enumPropertySpec -> + emitBlock(enumPropertySpec) + if (index < size - 1) { + codeWriter.emit(",$NEW_LINE") + } + } + } + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/ConstantPropertyWriter.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/ConstantPropertyWriter.kt new file mode 100644 index 00000000..79b68367 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/ConstantPropertyWriter.kt @@ -0,0 +1,36 @@ +package net.theevilreaper.dartpoet.code.writer + +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.InitializerAppender +import net.theevilreaper.dartpoet.code.Writeable +import net.theevilreaper.dartpoet.property.consts.ConstantPropertySpec +import net.theevilreaper.dartpoet.util.SEMICOLON +import net.theevilreaper.dartpoet.util.SPACE +import net.theevilreaper.dartpoet.util.StringHelper + +/** + * The [ConstantPropertyWriter] is responsible for writing the data of the [ConstantPropertySpec] to a [CodeWriter]. + * @since 1.0.0 + * @author theEvilReaper + */ +internal class ConstantPropertyWriter : Writeable, InitializerAppender { + + /** + * Writes the data from a provided [ConstantPropertySpec] to the given [CodeWriter] instance. + * + * @param spec the ConstantPropertySpec which should be written + * @param writer the instance from the [CodeWriter] to append the data + **/ + override fun write(spec: ConstantPropertySpec, writer: CodeWriter) { + val modifierString = StringHelper.joinModifiers(spec.modifiers, separator = SPACE, postfix = SPACE) + writer.emit(modifierString) + + if (spec.typeName != null) { + writer.emitCode("%T·", spec.typeName) + } + + writer.emitCode("%L", StringHelper.ensureVariableNameWithPrivateModifier(spec.name, spec.isPrivate)) + writeInitBlock(spec.initializer.build(), writer) + writer.emit(SEMICOLON) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/ConstructorWriter.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/ConstructorWriter.kt new file mode 100644 index 00000000..5d099dbf --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/ConstructorWriter.kt @@ -0,0 +1,79 @@ +package net.theevilreaper.dartpoet.code.writer + +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.DocumentationAppender +import net.theevilreaper.dartpoet.code.Writeable +import net.theevilreaper.dartpoet.code.emitParameters +import net.theevilreaper.dartpoet.function.constructor.ConstructorSpec +import net.theevilreaper.dartpoet.util.CURLY_CLOSE +import net.theevilreaper.dartpoet.util.CURLY_OPEN +import net.theevilreaper.dartpoet.util.NEW_LINE +import net.theevilreaper.dartpoet.util.SEMICOLON + +internal class ConstructorWriter : Writeable, DocumentationAppender { + + override fun write(spec: ConstructorSpec, writer: CodeWriter) { + emitDocumentation(spec.docs, writer) + if (spec.modifiers.contains(DartModifier.CONST)) { + writer.emit("${DartModifier.CONST.identifier}·") + } + + if (spec.isFactory) { + writer.emit("${DartModifier.FACTORY.identifier}·") + } + + writer.emit(spec.name) + + if (spec.isNamed) { + writer.emit(".${spec.named}") + } + + if (!spec.hasParameters) { + writer.emit("()$SEMICOLON") + return + } + + writer.emit("(") + + spec.parameters.emitParameters(writer) + + if (spec.hasNamedParameters) { + if (spec.parameters.isNotEmpty()) { + writer.emit(",$NEW_LINE") + } + writer.emit("$CURLY_OPEN") + writer.emit(NEW_LINE) + writer.indent() + + spec.requiredAndNamedParameters.emitParameters(writer, emitSpace = false, forceNewLines = true) + + writer.unindent() + writer.emit(NEW_LINE) + writer.emit("$CURLY_CLOSE") + } + + + writer.emit(")") + + if (spec.isLambda) { + writer.emit("·=>$NEW_LINE") + writer.indent(2) + writer.emitCode(spec.initializer.build(), ensureTrailingNewline = true) + writer.unindent(2) + } else { + if (spec.initializer.isNotEmpty()) { + writer.emit(":·") + writer.emitCode(spec.initializer.build(), ensureTrailingNewline = false) + writer.emit(SEMICOLON) + return + } + + if (spec.isFactory) { + writer.emit(" = _${spec.name}") + } + + writer.emit(SEMICOLON) + } + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/DartFileWriter.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/DartFileWriter.kt new file mode 100644 index 00000000..e73d6211 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/DartFileWriter.kt @@ -0,0 +1,54 @@ +package net.theevilreaper.dartpoet.code.writer + +import net.theevilreaper.dartpoet.DartFile +import net.theevilreaper.dartpoet.code.* +import net.theevilreaper.dartpoet.code.emitExtensions +import net.theevilreaper.dartpoet.directive.Directive +import net.theevilreaper.dartpoet.util.NEW_LINE + +internal class DartFileWriter : Writeable, DocumentationAppender { + + private val classWriter = ClassWriter() + + override fun write(spec: DartFile, writer: CodeWriter) { + emitDocumentation(spec.docs, writer) + emitDirectives(writer, spec.libImport) + emitDirectives(writer, spec.dartImports) + emitDirectives(writer, spec.packageImports) + emitDirectives(writer, spec.relativeImports) + emitDirectives(writer, spec.exportDirectives) + emitDirectives(writer, spec.partImports) + + spec.constants.emitConstants(writer) + + if (spec.constants.isNotEmpty()) { + writer.emit(NEW_LINE) + } + + if (spec.hasTypeDefs) { + spec.typeDefs.emitTypeDefs(writer) + } + + if (spec.types.isNotEmpty()) { + spec.types.forEach { + classWriter.write(it, writer) + if (spec.types.size > 1) { + writer.emit(NEW_LINE) + } + + } + } + spec.extensions.emitExtensions(writer) + } + + /** + * Emit a given [List] of [Directive] implementations to a [CodeWriter]. + * @param codeWriter the [CodeWriter] instance to append the directives + * @param directives the [List] of [Directive] implementations + */ + private fun emitDirectives(codeWriter: CodeWriter, directives: List) { + if (directives.isEmpty()) return + directives.writeImports(codeWriter, newLineAtBegin = false) + codeWriter.emit(NEW_LINE) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/EnumPropertyWriter.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/EnumPropertyWriter.kt new file mode 100644 index 00000000..cf881e8b --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/EnumPropertyWriter.kt @@ -0,0 +1,42 @@ +package net.theevilreaper.dartpoet.code.writer + +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.Writeable +import net.theevilreaper.dartpoet.code.emitAnnotations +import net.theevilreaper.dartpoet.enum.EnumPropertySpec +import net.theevilreaper.dartpoet.util.ROUND_CLOSE +import net.theevilreaper.dartpoet.util.ROUND_OPEN + +/** + * The class contains the logic to transform a [EnumPropertySpec] into code. + * Each generated part will be applied to a [CodeWriter] reference. + * @since 1.0.0 + * @version 1.0.0 + * @author theEvilReaper + */ +internal class EnumPropertyWriter : Writeable { + + /** + * Writes the given data from a [EnumPropertySpec] to a [CodeWriter] instance. + * @param spec the [EnumPropertySpec] to get the data to write + * @param writer the [CodeWriter] instance to apply the code + */ + override fun write(spec: EnumPropertySpec, writer: CodeWriter) { + spec.annotations.emitAnnotations(codeWriter = writer, inLineAnnotations = false) + writer.emit(spec.name) + if (spec.hasGeneric) { + writer.emitCode("<%T>", spec.generic!!) + } + + if (spec.hasParameter) { + writer.emit(ROUND_OPEN) + spec.parameters.forEachIndexed { index, codeBlock -> + if (index > 0) { + writer.emit(", ") + } + writer.emitCode(codeBlock, isConstantContext = false, ensureTrailingNewline = false) + } + writer.emit(ROUND_CLOSE) + } + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/ExtensionWriter.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/ExtensionWriter.kt new file mode 100644 index 00000000..9ac3820a --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/ExtensionWriter.kt @@ -0,0 +1,61 @@ +package net.theevilreaper.dartpoet.code.writer + +import net.theevilreaper.dartpoet.DartModifier.* +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.DocumentationAppender +import net.theevilreaper.dartpoet.code.Writeable +import net.theevilreaper.dartpoet.code.emitFunctions +import net.theevilreaper.dartpoet.extension.ExtensionSpec +import net.theevilreaper.dartpoet.util.CURLY_CLOSE +import net.theevilreaper.dartpoet.util.NEW_LINE + +/** + * This writer type has the ability to write extension methods. + * An extension method can be used to add additional method to an existing class. + * This specific methods are only visible to the corresponding project. + * More information about extension methods can be found in the [wiki](https://dart.dev/language/extension-methods) from dart + * @author theEvilReaper + * @since 1.0.0 + */ +internal class ExtensionWriter : Writeable, DocumentationAppender { + + /** + * The method handles the complete generation for an extension class with its methods. + * @param spec the [ExtensionSpec] which contains all relevant data for the generation + * @param writer the [CodeWriter] instance to append the generated code into a [Appendable] + */ + override fun write(spec: ExtensionSpec, writer: CodeWriter) { + emitDocumentation(spec.docs, writer) + writer.emitCode("%L", EXTENSION.identifier) + + if (spec.name != null) { + writer.emitCode("·%L", spec.name) + } + + if (spec.hasGenericCast) { + writer.emitCode("<%L>", spec.joinedRawTypes) + } + + writer.emitCode("·%L·%T·", ON.identifier, spec.extClass) + + // Handles the case when an extension class has no content. + if (spec.hasNoContent) { + writer.emit("{}") + return; + } + + writer.emit("{\n") + writer.indent() + + spec.functions.emitFunctions(writer) + + writer.emit(NEW_LINE) + + writer.unindent() + writer.emit("$CURLY_CLOSE") + + if (spec.endWithNewLine) { + writer.emit(NEW_LINE) + } + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/FunctionWriter.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/FunctionWriter.kt new file mode 100644 index 00000000..ad7b6ab0 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/FunctionWriter.kt @@ -0,0 +1,182 @@ +package net.theevilreaper.dartpoet.code.writer + +import net.theevilreaper.dartpoet.DartModifier.* +import net.theevilreaper.dartpoet.code.* +import net.theevilreaper.dartpoet.code.DocumentationAppender +import net.theevilreaper.dartpoet.code.emitParameters +import net.theevilreaper.dartpoet.function.FunctionSpec +import net.theevilreaper.dartpoet.parameter.ParameterSpec +import net.theevilreaper.dartpoet.util.* +import net.theevilreaper.dartpoet.util.EMPTY_STRING +import net.theevilreaper.dartpoet.util.NEW_LINE +import net.theevilreaper.dartpoet.util.SEMICOLON +import net.theevilreaper.dartpoet.util.SPACE +import net.theevilreaper.dartpoet.util.toImmutableSet + +internal class FunctionWriter : Writeable, DocumentationAppender { + + override fun write(spec: FunctionSpec, writer: CodeWriter) { + emitDocumentation(spec.docs, writer) + + if (spec.annotation.isNotEmpty()) { + spec.annotation.forEach { it.write(writer) } + writer.emit(NEW_LINE) + } + + val writeableModifiers = spec.modifiers.filter { it != PRIVATE && it != PUBLIC }.toImmutableSet() + val modifierString = writeableModifiers.joinToString( + separator = SPACE, + postfix = if (writeableModifiers.isNotEmpty()) SPACE else EMPTY_STRING + ) { it.identifier } + + writer.emit(modifierString) + + if (spec.returnType == null) { + if (spec.isAsync) { + writer.emitCode("Future<%L>", VOID.identifier) + } else { + if (spec.asSetter) { + writer.emitCode("set·") + } else { + writer.emit("${VOID.identifier}·") + } + } + } else { + + if (spec.isAsync) { + writer.emit("Future<") + } + writer.emitCode("%T", spec.returnType) + + if (spec.isGetter) { + writer.emit("·get") + } + + if (spec.isAsync) { + writer.emit(">") + } + writer.emit("·") + } + writer.emit("${if (spec.isPrivate) PRIVATE.identifier else ""}${spec.name}") + + if (spec.typeCast != null) { + writer.emitCode("<%T>", spec.typeCast) + } + + if (spec.isGetter) { + writer.emit("·=>·") + writer.emitCode(spec.body.returnsWithoutLinebreak(), ensureTrailingNewline = false) + } else { + writeParameters(spec, writer) + writeBody(spec, writer) + } + } + + private fun writeBody(spec: FunctionSpec, writer: CodeWriter) { + if (spec.body.isEmpty()) { + writer.emit(SEMICOLON) + return + } + if (spec.isAsync) { + writer.emit("·${ASYNC.identifier}") + } + if (spec.isLambda) { + writer.emit("·=>·") + } else { + writer.emit("·{\n") + writer.indent() + } + writer.emitCode(spec.body.returnsWithoutLinebreak(), ensureTrailingNewline = false) + if (!spec.isLambda) { + writer.unindent() + writer.emit("\n}") + } + } + + private fun writeParameters(spec: FunctionSpec, codeWriter: CodeWriter) { + if (!spec.hasParameters) { + codeWriter.emit("()") + return + } + codeWriter.emit("(") + spec.normalParameter.emitParameters(codeWriter, forceNewLines = false) + + if (spec.normalParameter.isNotEmpty() && (spec.hasAdditionalParameters || spec.parametersWithDefaults.isNotEmpty())) { + codeWriter.emit(", ") + } + + if (spec.hasAdditionalParameters) { + emitRequiredAndNamedParameter(spec, codeWriter) + } + + if (spec.parametersWithDefaults.isNotEmpty()) { + codeWriter.emit("[") + spec.parametersWithDefaults.emitParameters(codeWriter, forceNewLines = false) + codeWriter.emit("]") + } + + codeWriter.emit(")") + } + + private fun emitRequiredAndNamedParameter(spec: FunctionSpec, codeWriter: CodeWriter) { + codeWriter.emit("$CURLY_OPEN") + + val namedRequired = spec.namedParameter.filter { it.isRequired && !it.hasInitializer }.toImmutableList() + + writeParameters(namedRequired, spec.normalParameter.isNotEmpty(), codeWriter) + writeParameters(spec.requiredParameter, namedRequired.isNotEmpty(), codeWriter) + + val test = + spec.namedParameter.minus(namedRequired).filter { it.isNullable || it.hasInitializer }.toImmutableList() + + writeParameters(test, spec.requiredParameter.isNotEmpty() || namedRequired.isNotEmpty(), codeWriter) + + codeWriter.emit("$CURLY_CLOSE") + } + + private fun writeParameters( + parameters: List, + emitSpaceComma: Boolean = false, + codeWriter: CodeWriter + ) { + if (parameters.isEmpty()) return + if (emitSpaceComma) { + codeWriter.emit(", ") + } + parameters.emitParameters(codeWriter, forceNewLines = false) + } + + private val RETURN_EXPRESSION_BODY_PREFIX_SPACE = CodeBlock.of("return ") + private val RETURN_EXPRESSION_BODY_PREFIX_NBSP = CodeBlock.of("return·") + private val THROW_EXPRESSION_BODY_PREFIX_SPACE = CodeBlock.of("throw ") + private val THROW_EXPRESSION_BODY_PREFIX_NBSP = CodeBlock.of("throw·") + + private fun CodeBlock.returnsWithoutLinebreak(): CodeBlock { + val returnWithSpace = RETURN_EXPRESSION_BODY_PREFIX_SPACE.formatParts[0] + val returnWithNbsp = RETURN_EXPRESSION_BODY_PREFIX_NBSP.formatParts[0] + var originCodeBlockBuilder: CodeBlock.Builder? = null + for ((i, formatPart) in formatParts.withIndex()) { + if (formatPart.startsWith(returnWithSpace)) { + val builder = originCodeBlockBuilder ?: toBuilder() + originCodeBlockBuilder = builder + builder.formatParts[i] = formatPart.replaceFirst(returnWithSpace, returnWithNbsp) + } + } + return originCodeBlockBuilder?.build() ?: this + } + + private fun CodeBlock.asExpressionBody(): CodeBlock? { + val codeBlock = this.trim() + val asReturnExpressionBody = codeBlock.withoutPrefix(RETURN_EXPRESSION_BODY_PREFIX_SPACE) + ?: codeBlock.withoutPrefix(RETURN_EXPRESSION_BODY_PREFIX_NBSP) + if (asReturnExpressionBody != null) { + return asReturnExpressionBody + } + if (codeBlock.withoutPrefix(THROW_EXPRESSION_BODY_PREFIX_SPACE) != null || + codeBlock.withoutPrefix(THROW_EXPRESSION_BODY_PREFIX_NBSP) != null + ) { + return codeBlock + } + return null + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/ParameterWriter.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/ParameterWriter.kt new file mode 100644 index 00000000..ce546e3f --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/ParameterWriter.kt @@ -0,0 +1,48 @@ +package net.theevilreaper.dartpoet.code.writer + +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.code.* +import net.theevilreaper.dartpoet.code.emitAnnotations +import net.theevilreaper.dartpoet.parameter.ParameterSpec +import net.theevilreaper.dartpoet.util.EMPTY_STRING + +/** + * The ParameterWriter is used to write each parameter. + * @author theEvilReaper + * @version 1.0.0 + * @since 1.0.0 + */ +internal class ParameterWriter : Writeable, InitializerAppender { + + /** + * The method contains the main logic to write a [ParameterSpec] to code. + * It should be noted that the writer doesn't check whether the spec contains errors. + * This would be done when the spec is being created + */ + override fun write(spec: ParameterSpec, writer: CodeWriter) { + spec.annotations.emitAnnotations(writer, endWithNewLine = false) + + if (spec.isRequired && (!spec.isNamed || !spec.hasInitializer)) { + writer.emit("${DartModifier.REQUIRED.identifier}·") + } + + if (spec.type != null) { + writer.emitCode("%T", spec.type) + } + val emitNullable = if (spec.isNullable) "?·" else if (spec.type != null) "·" else EMPTY_STRING + writer.emit(emitNullable) + if (spec.hasNoTypeName) { + writer.emit("this.") + } + writer.emit(spec.name) + writeInitBlock(spec, writer) + } + + override fun writeInitBlock(spec: ParameterSpec, writer: CodeWriter, isConstantContext: Boolean) { + val initBlock = spec.initializer ?: CodeBlock.EMPTY + if (initBlock.isEmpty()) return + if (spec.isNamed && !spec.hasInitializer) return + writer.emit("·=·") + writer.emitCode(initBlock, isConstantContext) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/PropertyWriter.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/PropertyWriter.kt new file mode 100644 index 00000000..a5bb8ac6 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/PropertyWriter.kt @@ -0,0 +1,46 @@ +package net.theevilreaper.dartpoet.code.writer + +import net.theevilreaper.dartpoet.code.* +import net.theevilreaper.dartpoet.code.emitAnnotations +import net.theevilreaper.dartpoet.property.PropertySpec +import net.theevilreaper.dartpoet.util.EMPTY_STRING +import net.theevilreaper.dartpoet.util.SEMICOLON +import net.theevilreaper.dartpoet.util.SPACE +import net.theevilreaper.dartpoet.util.StringHelper + +/** + * The [PropertyWriter] contains the main logic to write a valid structure of a property for the programming language Dart. + * It contains only the logic of the write process. Each validation is done by the [PropertySpec] itself on the creation of it. + * @author theEvilReaper + * @since 1.0.0 + */ +internal class PropertyWriter : Writeable, DocumentationAppender, + InitializerAppender { + + /** + * Writes the given [PropertySpec] into the [CodeWriter]. + * @param spec the [PropertySpec] which is involved + * @param writer the [CodeWriter] instance to write the code + */ + override fun write(spec: PropertySpec, writer: CodeWriter) { + emitDocumentation(spec.docs, writer) + spec.annotations.emitAnnotations(writer) { + it.write(writer, inline = false) + } + + val modifierString = StringHelper.joinModifiers( + spec.modifiers, + separator = SPACE, + postfix = if (spec.hasModifiers) SPACE else EMPTY_STRING + ) + writer.emit(modifierString) + + if (spec.type != null) { + writer.emitCode("%T·", spec.type) + } + + writer.emit(StringHelper.ensureVariableNameWithPrivateModifier(spec.name, spec.isPrivate)) + writeInitBlock(spec.initBlock.build(), writer) + writer.emit(SEMICOLON) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/TypeDefWriter.kt b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/TypeDefWriter.kt new file mode 100644 index 00000000..5002caec --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/code/writer/TypeDefWriter.kt @@ -0,0 +1,76 @@ +package net.theevilreaper.dartpoet.code.writer + +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.Writeable +import net.theevilreaper.dartpoet.code.emitParameters +import net.theevilreaper.dartpoet.function.typedef.TypeDefSpec +import net.theevilreaper.dartpoet.parameter.ParameterSpec +import net.theevilreaper.dartpoet.util.SEMICOLON +import net.theevilreaper.dartpoet.util.toImmutableList + +class TypeDefWriter : Writeable { + + override fun write(spec: TypeDefSpec, writer: CodeWriter) { + writer.emit("${DartModifier.TYPEDEF.identifier}·${spec.typeDefName}") + if (spec.typeCasts.isNotEmpty()) { + val typesAsString = spec.typeCasts.joinToString(separator = ",·") { it.toString() } + writer.emitCode("<%L>", typesAsString) + } + writer.emit("·=·") + writer.emitCode("%T", spec.returnType) + + if (spec.name != null) { + writer.emitCode("·%L", spec.name) + } + + emitParameters(spec, writer) + writer.emit(SEMICOLON) + } + + private fun emitParameters(spec: TypeDefSpec, codeWriter: CodeWriter) { + if (spec.hasParameters) { + codeWriter.emit("(") + callParameterWrite(codeWriter, spec.normalParameter) { it.isNotEmpty() } + if (spec.hasAdditionalParameters) { + if (spec.hasParameters) { + codeWriter.emit(",") + } + codeWriter.emit("·{") + val namedRequired = spec.namedParameter.filter { it.isRequired && !it.hasInitializer }.toImmutableList() + + callParameterWrite(codeWriter, namedRequired) { it.isNotEmpty() } + callParameterWrite(codeWriter, spec.requiredParameter) { it.isNotEmpty() } + + if (namedRequired.isNotEmpty()) { + codeWriter.emitCode(",·") + } + + val test = + spec.namedParameter.minus(namedRequired).filter { it.isNullable || it.hasInitializer }.toImmutableList() + callParameterWrite(codeWriter, test) { it.isNotEmpty() } + codeWriter.emit("}") + } + + if (spec.parametersWithDefaults.isNotEmpty()) { + if (spec.hasParameters) { + codeWriter.emit(", ") + } + codeWriter.emit("[") + callParameterWrite(codeWriter, spec.parametersWithDefaults) { it.isNotEmpty() } + codeWriter.emit("]") + } + + codeWriter.emit(")") + } + } + + private inline fun callParameterWrite( + writer: CodeWriter, + parameters: List, + crossinline predicate: (List) -> Boolean, + ) { + if (!predicate.invoke(parameters)) return + parameters.emitParameters(writer, forceNewLines = false, emitSpace = parameters.size > 1) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/directive/BaseDirective.kt b/src/main/kotlin/net/theevilreaper/dartpoet/directive/BaseDirective.kt new file mode 100644 index 00000000..dd372d82 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/directive/BaseDirective.kt @@ -0,0 +1,63 @@ +package net.theevilreaper.dartpoet.directive + +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.buildCodeString +import net.theevilreaper.dartpoet.util.DART_FILE_ENDING + +/** + * The class represents the basic implementation which are used from all directive implementations. + * @param path the path for the directive + * @author theEvilReaper + * @since 1.0.0 + */ +abstract class BaseDirective( + private val path: String +) : Directive { + + init { + check(path.trim().isNotEmpty()) { "The path of an directive can't be empty" } + } + + /** + * Contains the logic to writer a [BaseDirective] implementation into code. + * @param writer the [CodeWriter] instance to append the directive + */ + abstract fun write(writer: CodeWriter) + + /** + * Creates a string representation from a [BaseDirective] implementation. + * @return the created string + */ + override fun asString() = buildCodeString { write(this) } + + /** + * Makes a comparison with two [Directive] implementation over a [Comparable] + */ + override fun compareTo(other: Directive): Int = asString().compareTo(other.toString()) + + /** + * Ensures that the directive path ends with .dart. + * @return the original string or the string with .dart at the end + */ + protected fun String.ensureDartFileEnding(): String { + return if (!this.endsWith(DART_FILE_ENDING) && !isDartImport()) { + "$this$DART_FILE_ENDING" + } else { + this + } + } + + /** + * Checks if a given import path starts with the word dart. + * @return true when the path starts with the word otherwise false + */ + protected fun isDartImport(): Boolean { + return path.startsWith("dart") + } + + /** + * Returns the raw data string from the directive. + * @return the raw data string + */ + override fun getRawPath(): String = this.path +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/import/ImportCastType.kt b/src/main/kotlin/net/theevilreaper/dartpoet/directive/CastType.kt similarity index 74% rename from src/main/kotlin/net/theevilreaper/dartpoet/import/ImportCastType.kt rename to src/main/kotlin/net/theevilreaper/dartpoet/directive/CastType.kt index 4c174698..204559ff 100644 --- a/src/main/kotlin/net/theevilreaper/dartpoet/import/ImportCastType.kt +++ b/src/main/kotlin/net/theevilreaper/dartpoet/directive/CastType.kt @@ -1,13 +1,13 @@ -package net.theevilreaper.dartpoet.import +package net.theevilreaper.dartpoet.directive /** * Represent all available import casts from flutter. * @author theEvilReaper * @since 1.0.0 */ -enum class ImportCastType( +enum class CastType( val identifier: String -){ +) { AS("as"), SHOW("show"), HIDE("hide"), diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/directive/DartDirective.kt b/src/main/kotlin/net/theevilreaper/dartpoet/directive/DartDirective.kt new file mode 100644 index 00000000..d138b6b9 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/directive/DartDirective.kt @@ -0,0 +1,53 @@ +package net.theevilreaper.dartpoet.directive + +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.util.IMPORT +import net.theevilreaper.dartpoet.util.SEMICOLON + +/** + * Represents an import directive from dart which usual starts with `dart` or `package`. + * + * @param path the path to the Dart file or package being imported. + * @param castType the optional cast type for the imported directive, used when casting the directive + * @param importCast the optional import cast, specifying a cast expression for the imported directive. + * + * @throws IllegalArgumentException if [importCast] is provided and is empty or consists only of whitespace. + * + * @constructor Creates a Dart import directive with the specified path as [String], a cast type as [CastType], and a importCast a [String]. + */ +class DartDirective internal constructor( + private val path: String, + private val castType: CastType? = null, + private val importCast: String? = null, +) : BaseDirective(path) { + + /** + * Check if some conditions are false and throw an exception. + */ + init { + if (importCast != null) { + check(importCast.trim().isNotEmpty()) { "The importCast can't be empty" } + } + + if ((castType != null && importCast == null) || (castType == null && importCast != null)) { + throw IllegalStateException("The castType and importCast must be set together or must be null. A mixed state is not allowed") + } + } + + /** + * Writes the given data from the directive to the provided [CodeWriter]. + * + * @param writer the writer instance to append the directive + */ + override fun write(writer: CodeWriter) { + writer.emit("$IMPORT ") + val ensuredPath = path.ensureDartFileEnding() + val pathToWrite = if (isDartImport()) ensuredPath else "package:$ensuredPath" + writer.emit("'$pathToWrite'") + + if (importCast != null && castType != null) { + writer.emit(" ${castType.identifier} $importCast") + } + writer.emit(SEMICOLON) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/directive/Directive.kt b/src/main/kotlin/net/theevilreaper/dartpoet/directive/Directive.kt new file mode 100644 index 00000000..ea304800 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/directive/Directive.kt @@ -0,0 +1,32 @@ +package net.theevilreaper.dartpoet.directive + +/** + * The import interface is a marker interface which is used to restrict what an import can be. + * These implementation of the interface are currently available: + * - LibraryImports + * - library test; + * - part of test; + * - DartImports + * - import 'dart:html'; + * - import 'package 'test.dart'; + * - import '../model/test.dart'; + * - PartImports + * - part 'test.freezed.dart'; + * + * @since 1.0.0 + * @author theEvilReaper + */ +sealed interface Directive : Comparable { + + /** + * Returns a string representation of the directive. + * @return the string representation + */ + fun asString(): String + + /** + * Returns the raw path of the directive. + * @return the raw path + */ + fun getRawPath(): String +} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/directive/DirectiveFactory.kt b/src/main/kotlin/net/theevilreaper/dartpoet/directive/DirectiveFactory.kt new file mode 100644 index 00000000..54e901a9 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/directive/DirectiveFactory.kt @@ -0,0 +1,85 @@ +package net.theevilreaper.dartpoet.directive + +/** + * The [DirectiveFactory] should be used to create a new instance of different [Directive] implementations. + * It's required to use this factory to create a new instance because it will check if the given [DirectiveType]. + * The usage of the constructor of each implementation is not possible. + * @author theEvilReaper + * @version 1.0.0 + * @since 1.0.0 + **/ +object DirectiveFactory { + + /** + * Creates a new instance from a [Directive] implementation depends on the given [DirectiveType]. + * @param directive the type of the directive + * @param path the path to the file + * @return the created [Directive] instance + */ + @Throws(IllegalStateException::class) + fun create( + directive: DirectiveType, + path: String, + ): Directive { + check(directive != DirectiveType.LIBRARY) { + "The library directive doesn't support a cast type or import cast. Please use #createLibDirective method instead" + } + return create(directive, path, false, null, null) + } + + /** + * Creates a new instance from a [Directive] implementation depends on the given [DirectiveType]. + * If the [Directive] implementation doesn't support the given [CastType] or [importCast] option. + * It will throw an [IllegalStateException]. + * @param directive the type of the directive + * @param path the path to the file + * @param castType the [CastType] to use + * @param importCast the import cast to use + * @return the created [Directive] instance + */ + @Throws(IllegalStateException::class) + fun create( + directive: DirectiveType, + path: String, + castType: CastType? = null, + importCast: String? = null, + ): Directive { + check(directive != DirectiveType.LIBRARY) { + "The library directive doesn't support a cast type or import cast. Please use #createLibDirective method instead" + } + return create(directive, path, false, castType, importCast) + } + + @Throws(IllegalStateException::class) + fun createLib( + path: String, + partOf: Boolean = false, + ) = create(DirectiveType.LIBRARY, path, partOf, null, null) + + /** + * Creates a new instance from a [Directive] implementation depends on the given [DirectiveType]. + * If the [Directive] implementation doesn't support the given [CastType] or [importCast] option. + * It will throw an [IllegalStateException]. + * @param directive the type of the directive + * @param path the path to the file + * @param castType the [CastType] to use + * @param importCast the import cast to use + * @return the created [Directive] instance + */ + @Throws(IllegalStateException::class) + private fun create( + directive: DirectiveType, + path: String, + partOf: Boolean = false, + castType: CastType? = null, + importCast: String? = null, + ): Directive { + return when (directive) { + DirectiveType.IMPORT -> DartDirective(path, castType, importCast) + DirectiveType.RELATIVE -> RelativeDirective(path, castType, importCast) + DirectiveType.PART -> PartDirective(path) + DirectiveType.LIBRARY -> LibraryDirective(path, partOf) + DirectiveType.EXPORT -> ExportDirective(path, castType, importCast) + } + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/directive/DirectiveType.kt b/src/main/kotlin/net/theevilreaper/dartpoet/directive/DirectiveType.kt new file mode 100644 index 00000000..a6c9e9a4 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/directive/DirectiveType.kt @@ -0,0 +1,16 @@ +package net.theevilreaper.dartpoet.directive + + +/** + * The [DirectiveType] enum represents the different types of directives. + * @since 1.0.0 + * @version 1.0.0 + * @author theEvilReaper + */ +enum class DirectiveType { + IMPORT, + RELATIVE, + PART, + LIBRARY, + EXPORT +} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/directive/ExportDirective.kt b/src/main/kotlin/net/theevilreaper/dartpoet/directive/ExportDirective.kt new file mode 100644 index 00000000..28717200 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/directive/ExportDirective.kt @@ -0,0 +1,53 @@ +package net.theevilreaper.dartpoet.directive + +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.util.SEMICOLON + +/** + * This implementation represents the export directive from dart. + * An export can be look like this: + * + * export 'path.dart'; + * + * @since 1.0.0 + * @author theEvilReaper + */ +class ExportDirective internal constructor( + private val path: String, + private val castType: CastType? = null, + private val importCast: String? = null, +) : BaseDirective(path) { + + private val export = "export" + private val invalidCastType = arrayOf(CastType.DEFERRED, CastType.AS) + + init { + if (castType != null && castType in invalidCastType) { + throw IllegalStateException("The following cast types are not allowed for an export directive: [${invalidCastType.joinToString()}]") + } + + if (importCast != null) { + check(importCast.trim().isNotEmpty()) { "The importCast can't be empty" } + } + + if ((castType != null && importCast == null) || (castType == null && importCast != null)) { + throw IllegalStateException("The castType and importCast must be set together or must be null. A mixed state is not allowed") + } + } + + /** + * Writes an [ExportDirective] with the right syntax to an [CodeWriter] instance. + * @param writer the [CodeWriter] instance to append the directive + */ + override fun write(writer: CodeWriter) { + val ensuredPath = path.ensureDartFileEnding() + writer.emit("$export·'") + writer.emit(ensuredPath) + writer.emit("'") + + if (importCast != null && castType != null) { + writer.emit("·${castType.identifier} $importCast") + } + writer.emit(SEMICOLON) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/directive/LibraryDirective.kt b/src/main/kotlin/net/theevilreaper/dartpoet/directive/LibraryDirective.kt new file mode 100644 index 00000000..4d45f5d1 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/directive/LibraryDirective.kt @@ -0,0 +1,27 @@ +package net.theevilreaper.dartpoet.directive + +import net.theevilreaper.dartpoet.DartModifier.LIBRARY +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.util.SEMICOLON + +/** + * The [LibraryDirective] represents the library directive from dart. + * @since 1.0.0 + * @author theEvilReaper + */ +class LibraryDirective internal constructor( + private val path: String, + private val asPartOf: Boolean = false +) : BaseDirective(path) { + + /** + * Writes the data from the [LibraryDirective] to a given instance from a [CodeWriter]. + * @param writer the [CodeWriter] instance to append the directive + */ + override fun write(writer: CodeWriter) { + val baseString = if (asPartOf) "part of" else LIBRARY.identifier + writer.emit("$baseString·") + writer.emit(path) + writer.emit(SEMICOLON) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/directive/PartDirective.kt b/src/main/kotlin/net/theevilreaper/dartpoet/directive/PartDirective.kt new file mode 100644 index 00000000..1724d4fa --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/directive/PartDirective.kt @@ -0,0 +1,26 @@ +package net.theevilreaper.dartpoet.directive + +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.util.SEMICOLON + +/** + * This implementation represents a part directive from dart. + * The main difference to the other variants is that it starts with the word part and not with import. + * @since 1.0.0 + * @author theEvilReaper + */ +class PartDirective internal constructor( + private val path: String +) : BaseDirective(path) { + + /** + * Writes the content for a part directive to an instance of an [CodeWriter]. + * @param writer the [CodeWriter] instance to append the directive + */ + override fun write(writer: CodeWriter) { + writer.emit("part ") + writer.emit("'") + writer.emit(path) + writer.emit("'$SEMICOLON") + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/directive/RelativeDirective.kt b/src/main/kotlin/net/theevilreaper/dartpoet/directive/RelativeDirective.kt new file mode 100644 index 00000000..87c607e1 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/directive/RelativeDirective.kt @@ -0,0 +1,43 @@ +package net.theevilreaper.dartpoet.directive + +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.util.SEMICOLON + +/** + * This implementation represents a relative directive from dart. + * The difference to other directive variants is that the path starts with .../ or ../. + * @since 1.0.0 + * @author theEvilReaper + */ +class RelativeDirective internal constructor( + private val path: String, + private val castType: CastType? = null, + private val importCast: String? = null, +) : BaseDirective(path) { + + init { + if (importCast != null) { + check(importCast.trim().isNotEmpty()) { "The importCast can't be empty" } + } + + if ((castType != null && importCast == null) || (castType == null && importCast != null)) { + throw IllegalStateException("The castType and importCast must be set together or must be null. A mixed state is not allowed") + } + } + + /** + * Writes the content for a relative directive to an instance of an [CodeWriter]. + * @param writer the [CodeWriter] instance to append the directive + */ + override fun write(writer: CodeWriter) { + writer.emit("import ") + writer.emit("'") + writer.emit(path) + writer.emit("'") + + if (castType != null && importCast != null) { + writer.emit(" ${castType.identifier} $importCast") + } + writer.emit(SEMICOLON) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/enum/EnumPropertyBuilder.kt b/src/main/kotlin/net/theevilreaper/dartpoet/enum/EnumPropertyBuilder.kt new file mode 100644 index 00000000..68ad2d26 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/enum/EnumPropertyBuilder.kt @@ -0,0 +1,90 @@ +package net.theevilreaper.dartpoet.enum + +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import net.theevilreaper.dartpoet.code.CodeBlock +import net.theevilreaper.dartpoet.type.ClassName +import net.theevilreaper.dartpoet.type.TypeName +import net.theevilreaper.dartpoet.type.asClassName +import kotlin.reflect.KClass + +/** + * Builder for creating instances of [EnumPropertySpec]. + * Contains methods to add parameter objects and set a generic cast to the property. + * @param name the name for the property + * @version 1.0.0 + * @since 1.0.0 + * @author theEvilReaper + */ +class EnumPropertyBuilder( + val name: String +) { + internal var genericValueCast: TypeName? = null + internal val parameters: MutableList = mutableListOf() + internal val annotations: MutableList = mutableListOf() + + /** + * Adds a new [AnnotationSpec] instance to the property. + * @param annotation the annotation to add + * @return the builder instance + */ + fun annotation(annotation: AnnotationSpec) = apply { + this.annotations += annotation + } + + /** + * Adds multiple [AnnotationSpec] instances to the property. + * @param annotations the annotations to add + * @return the builder instance + */ + fun annotations(vararg annotations: AnnotationSpec) = apply { + this.annotations += annotations + } + + /** + * Adds a new parameter to the enum property. + * @param format The format for the parameter + * @param args The arguments for the format + * @return the builder instance + */ + fun parameter(format: String, vararg args: Any) = apply { + parameter(CodeBlock.of(format, *args)) + } + + /** + * Add a new parameter as [CodeBlock] to the property. + * @param block the [CodeBlock] to add + * @return the builder instance + */ + fun parameter(block: CodeBlock) = apply { + this.parameters += block + } + + /** + * Sets the generic cast to the enum property. + * @param value The value to set as [TypeName] + * @return the builder instance + */ + fun generic(value: TypeName) = apply { this.genericValueCast = value } + + /** + * Set the cast value for a property. + * @param value the value to set as [ClassName] + * @return the builder instance + */ + fun generic(value: ClassName) = apply { this.genericValueCast = value } + + /** + * Set the cast value for a property. + * @param value the value to set as [TypeName] + * @return the builder instance + */ + fun generic(value: KClass<*>) = apply { this.genericValueCast = value.asClassName() } + + /** + * Creates a new instance from the [EnumPropertySpec] with the builder instance as parameter. + * @return a created instance from an [EnumPropertySpec] + */ + fun build(): EnumPropertySpec { + return EnumPropertySpec(this) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/enum/EnumPropertySpec.kt b/src/main/kotlin/net/theevilreaper/dartpoet/enum/EnumPropertySpec.kt new file mode 100644 index 00000000..d3aedb40 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/enum/EnumPropertySpec.kt @@ -0,0 +1,68 @@ +package net.theevilreaper.dartpoet.enum + +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.buildCodeString +import net.theevilreaper.dartpoet.code.writer.EnumPropertyWriter +import net.theevilreaper.dartpoet.util.toImmutableList +import net.theevilreaper.dartpoet.util.toImmutableSet + +/** + * + * @since 1.0.0 + * @author theEvilReaper + */ +class EnumPropertySpec( + val builder: EnumPropertyBuilder +) { + internal val name = builder.name + internal val generic = builder.genericValueCast + internal val hasGeneric = builder.genericValueCast != null + internal val parameters = builder.parameters.toImmutableList() + internal val hasParameter = builder.parameters.isNotEmpty() + internal val annotations = builder.annotations.toImmutableSet() + + /** + * Contains some checks for the variable. + * When a variable doesn't pass the check an exception will be thrown. + */ + init { + check(name.trim().isNotEmpty()) { "The name of a EnumProperty can't be empty" } + } + + + internal fun write(codeWriter: CodeWriter) { + EnumPropertyWriter().write(this, codeWriter) + } + + /** + * Creates a string representation from the spec object. + * @return the created string + */ + override fun toString() = buildCodeString { write(this) } + + /** + * Converts a [EnumPropertySpec] to a [EnumPropertyWriter] instance. + * @return the created instance + */ + fun toBuilder(): EnumPropertyBuilder { + val builder = EnumPropertyBuilder(this.name) + builder.annotations.addAll(this.annotations) + builder.genericValueCast = this.generic + builder.parameters.addAll(this.parameters) + return builder + } + + /** + * The companion object contains some helper methods to create a new instance of a [EnumPropertyBuilder]. + */ + companion object { + + /** + * Creates a new instance from the [EnumPropertyBuilder] to construct a new property. + * @param name the name for the property + * @return the created instance from the [EnumPropertyBuilder] + */ + @JvmStatic + fun builder(name: String) = EnumPropertyBuilder(name) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/extension/ExtensionBuilder.kt b/src/main/kotlin/net/theevilreaper/dartpoet/extension/ExtensionBuilder.kt new file mode 100644 index 00000000..0b13e44d --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/extension/ExtensionBuilder.kt @@ -0,0 +1,117 @@ +package net.theevilreaper.dartpoet.extension + +import net.theevilreaper.dartpoet.code.CodeBlock +import net.theevilreaper.dartpoet.function.FunctionSpec +import net.theevilreaper.dartpoet.type.ClassName +import net.theevilreaper.dartpoet.type.TypeName +import net.theevilreaper.dartpoet.type.asTypeName +import org.jetbrains.annotations.ApiStatus +import kotlin.reflect.KClass + +/** + * The builder implementation for a [ExtensionSpec] allows to set specific attributes to set relevant data about the extension which should be generated. + * @param name the name of the extension provided as [String] + * @param extClass the class to extend as [TypeName] + * @since 1.0.0 + * @author theEvilReaper + */ +class ExtensionBuilder( + val name: String? = null, + val extClass: TypeName, +) { + internal var genericTypes: MutableList = mutableListOf() + internal var endWithNewLine: Boolean = false + internal val functionStack: MutableList = mutableListOf() + internal val docs: MutableList = mutableListOf() + + /** + * Add a comment over for the extension class. + * Note this comments will be generated over the extension class + * @param format the string which contains the content and the format + * @param args the arguments for the format string + * @return the current builder instance + */ + fun doc(format: String, vararg args: Any) = apply { + this.docs.add(CodeBlock.of(format.replace(' ', '·'), *args)) + } + + /** + * Adds a new [FunctionSpec] to the extension. + * @param function the function to add + * @return the current builder instance + */ + fun function(function: FunctionSpec) = apply { + this.functionStack += function + } + + /** + * Adds a new [FunctionSpec] to the extension using a lambda expression. + * @param function a lambda expression that creates the function to add + * @return the current builder instance + */ + fun function(function: () -> FunctionSpec) = apply { + this.functionStack += function() + } + + /** + * Adds multiple [FunctionSpec] instances to the extension. + * @param functions zhe functions to add + * @return the current builder instance + */ + fun functions(vararg functions: FunctionSpec) = apply { + this.functionStack += functions + } + + /** + * Specifies whether the generated extension structure should end with an empty line. + * @param withEmptyLine true to include an empty line at the end + * @return the current builder instance + */ + fun endsWithNewLine(withEmptyLine: Boolean) = apply { + this.endWithNewLine = withEmptyLine + } + + /** + * Add a generic type for the extension + * @param genericType the generic type to set as [ClassName] + * @return the current builder instance + */ + fun genericTypes(vararg genericType: ClassName) = apply { + this.genericTypes += genericType + } + + /** + * Add a generic type for the extension + * @param genericType the generic type to set as [TypeName] + * @return the current builder instance + */ + fun genericTypes(vararg genericType: TypeName) = apply { + this.genericTypes += genericType + } + + /** + * Add a generic type for the extension + * @param genericType the generic type to set as [Class] + * @return the current builder instance + */ + fun genericTypes(vararg genericType: Class<*>) = apply { + this.genericTypes += genericType.map { it.asTypeName() } + } + + /** + * Add a generic type for the extension + * @param genericType the generic type to set as [KClass] + * @return the current builder instance + */ + fun genericTypes(vararg genericType: KClass<*>) = apply { + this.genericTypes += genericType.map { it.asTypeName() } + } + + /** + * Creates a new instance from the [ExtensionSpec] class. + * @return the created instance + */ + fun build(): ExtensionSpec { + return ExtensionSpec(this) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/extension/ExtensionSpec.kt b/src/main/kotlin/net/theevilreaper/dartpoet/extension/ExtensionSpec.kt new file mode 100644 index 00000000..aaba2e1a --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/extension/ExtensionSpec.kt @@ -0,0 +1,156 @@ +package net.theevilreaper.dartpoet.extension + +import net.theevilreaper.dartpoet.code.CodeBlock +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.buildCodeString +import net.theevilreaper.dartpoet.code.writer.ExtensionWriter +import net.theevilreaper.dartpoet.function.FunctionSpec +import net.theevilreaper.dartpoet.parameter.ParameterBuilder +import net.theevilreaper.dartpoet.type.ClassName +import net.theevilreaper.dartpoet.type.TypeName +import net.theevilreaper.dartpoet.type.asTypeName +import net.theevilreaper.dartpoet.util.EMPTY_STRING +import net.theevilreaper.dartpoet.util.toImmutableList +import net.theevilreaper.dartpoet.util.toImmutableSet +import kotlin.reflect.KClass + +/** + * Represents a data structure holding information about an extension from Dart. + * An extension can be used to add additional methods to a given class. + * To create an extension, refer to the [ExtensionBuilder]. + * @param builder an [ExtensionBuilder] reference to retrieve data from + * @since 1.0.0 + * @author theEvilReaper + */ +class ExtensionSpec( + builder: ExtensionBuilder +) { + internal val name: String? = builder.name + internal val extClass: TypeName = builder.extClass + internal val endWithNewLine: Boolean = builder.endWithNewLine + internal val genericType: List = builder.genericTypes.toImmutableList() + internal val functions: Set = builder.functionStack.toImmutableSet() + internal val docs: List = builder.docs.toImmutableList() + internal val hasGenericCast: Boolean = builder.genericTypes.isNotEmpty() + internal val hasNoContent: Boolean = builder.functionStack.isEmpty() + + internal val joinedRawTypes by lazy { + if (genericType.isEmpty()) return@lazy EMPTY_STRING + val withComma = genericType.size > 1 + genericType.joinToString(if (withComma) ", " else EMPTY_STRING) { it.getRawData() } + } + + /** + * Performs checks on variables to avoid unwanted or incorrect data. + */ + init { + if (name != null) { + check(name.isNotEmpty()) { "The name of a extension can't be empty" } + } + + if (genericType.isNotEmpty()) { + val rawExtClass = extClass.getRawData() + check(joinedRawTypes == rawExtClass) { + """ + The generic usage from the genericCast and extensionClass is not the same. + Expected '$joinedRawTypes' but got in the extension class: '$rawExtClass' + """.trimIndent() + } + } + } + + /** + * Applies the given spec reference to a [CodeWriter] instance to write the given data into code for dart. + * @param codeWriter the writer instance to apply the data as code + */ + internal fun write(codeWriter: CodeWriter) { + ExtensionWriter().write(this, codeWriter) + } + + /** + * Creates a textual representation from the spec. + * It calls the [ExtensionWriter.write] method to create the representation. + * @return the created string representation + */ + override fun toString() = buildCodeString { write(this) } + + /** + * Creates a new [ExtensionBuilder] reference from an existing [ExtensionSpec] object. + * @return the created [ExtensionBuilder] instance + */ + fun toBuilder(): ExtensionBuilder { + val builder = ExtensionBuilder(this.name, this.extClass) + builder.genericTypes += this.genericType + builder.endWithNewLine = this.endWithNewLine + builder.docs.addAll(this.docs) + builder.functionStack.addAll(this.functions) + return builder + } + + /** + * The companion object contains some helper methods to create a new instance of a [ExtensionBuilder]. + */ + companion object { + + /** + * Creates a new instance of [ParameterBuilder] with the specified name and type. + * @param name the name for the extension class + * @param extClass the type for the class to extend, represented as a [String] + * @return A new [ExtensionBuilder] instance initialized with the provided data + */ + @JvmStatic + fun builder(name: String, extClass: String) = ExtensionBuilder(name, ClassName(extClass)) + + /** + * Creates a new instance of [ParameterBuilder] with the specified name and type. + * @param name the name for the extension class + * @param extClass the type for the class to extend, represented as a [ClassName] + * @return A new [ExtensionBuilder] instance initialized with the provided data + */ + @JvmStatic + fun builder(name: String, extClass: ClassName) = ExtensionBuilder(name, extClass) + + /** + * Creates a new instance of [ParameterBuilder] with the specified name and type. + * @param name the name for the extension class + * @param extClass the type for the class to extend, represented as a [TypeName] + * @return A new [ExtensionBuilder] instance initialized with the provided data + */ + @JvmStatic + fun builder(name: String, extClass: TypeName) = ExtensionBuilder(name, extClass) + + /** + * Creates a new instance of [ParameterBuilder] with the specified name and type. + * @param name the name for the extension class + * @param extClass the type for the class to extend, represented as a [Class] + * @return A new [ExtensionBuilder] instance initialized with the provided data + */ + @JvmStatic + fun builder(name: String, extClass: Class<*>) = ExtensionBuilder(name, extClass.asTypeName()) + + /** + * Creates a new instance of [ParameterBuilder] with the specified name and type. + * @param name the name for the extension class + * @param extClass the type for the class to extend, represented as a [KClass] + * @return A new [ExtensionBuilder] instance initialized with the provided data + */ + @JvmStatic + fun builder(name: String, extClass: KClass<*>) = ExtensionBuilder(name, extClass.asTypeName()) + + /** + * Creates a new instance of [ParameterBuilder] with the specified name and type. + * @param extClass the type for the class to extend, represented as a [String] + * @return A new [ExtensionBuilder] instance initialized with the provided data + */ + @JvmStatic + fun unnamed(extClass: String) = ExtensionBuilder(null, ClassName(extClass)) + + /** + * Creates a new instance of [ParameterBuilder] with the specified name and type. + * @param extClass the type for the class to extend, represented as a [String] + * @return A new [ExtensionBuilder] instance initialized with the provided data + */ + @JvmStatic + fun unnamed(extClass: KClass<*>) = ExtensionBuilder(null, extClass.asTypeName()) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/function/FunctionBuilder.kt b/src/main/kotlin/net/theevilreaper/dartpoet/function/FunctionBuilder.kt new file mode 100644 index 00000000..510407de --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/function/FunctionBuilder.kt @@ -0,0 +1,184 @@ +package net.theevilreaper.dartpoet.function + +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import net.theevilreaper.dartpoet.code.CodeBlock +import net.theevilreaper.dartpoet.meta.SpecData +import net.theevilreaper.dartpoet.meta.SpecMethods +import net.theevilreaper.dartpoet.parameter.ParameterSpec +import net.theevilreaper.dartpoet.type.ClassName +import net.theevilreaper.dartpoet.type.TypeName +import net.theevilreaper.dartpoet.type.asClassName +import net.theevilreaper.dartpoet.type.asTypeName +import net.theevilreaper.dartpoet.util.NO_PARAMETER_TYPE +import java.lang.reflect.Type +import kotlin.reflect.KClass + +/** + * The builder class allows the creation of an [FunctionBuilder] without any effort. + * @param name the name of the function + * @author 1.0.0 + * @since 1.0.0 + */ +class FunctionBuilder internal constructor( + val name: String, +) : SpecMethods { + internal val specData: SpecData = SpecData() + internal val parameters: MutableList = mutableListOf() + internal var async: Boolean = false + internal var returnType: TypeName? = null + internal val body: CodeBlock.Builder = CodeBlock.builder() + internal var typeCast: TypeName? = null + internal var setter: Boolean = false + internal var getter: Boolean = false + internal var lambda: Boolean = false + internal val docs: MutableList = mutableListOf() + + /** + * Add a comment over for the extension class. + * Note this comments will be generated over the extension class + * @param format the string which contains the content and the format + * @param args the arguments for the format string + */ + fun doc(format: String, vararg args: Any) = apply { + this.docs.add(CodeBlock.of(format.replace(' ', '·'), *args)) + } + + /** + * Indicates if the method should be generated as lambda method. + * @param lambda True when the method should be lambda otherwise false + */ + fun lambda(lambda: Boolean) = apply { + this.lambda = lambda + } + + /** + * Indicates if the method should be generated as set function. + * @param setter True for a setter generation + */ + fun setter(setter: Boolean) = apply { + this.setter = setter + } + + /** + * Indicates if the method should be generated as get function. + * @param getter True for a get generation + */ + fun getter(getter: Boolean) = apply { + this.getter = getter + } + + /** + * This method allows to specify a type cast using a [TypeName] object. + * It sets the type cast for the current instance and returns the modified instance. + * + * @param cast the [TypeName] representing the type to cast to + * @return the involved builder instance + */ + fun typeCast(cast: TypeName) = apply { this.typeCast = cast } + + /** + * This method allows to specify a type cast using a [ClassName] object. + * It sets the type cast for the current instance and returns the modified instance. + * + * @param cast the [ClassName] representing the type to cast to + * @return the involved builder instance + */ + fun typeCast(cast: ClassName) = apply { this.typeCast = cast } + + /** + * This method allows to specify a type cast using a [KClass] object. + * It sets the type cast for the current instance and returns the modified instance. + * + * @param cast the [KClass] representing the type to cast to + * @return the involved builder instance + */ + fun typeCast(cast: KClass<*>) = apply { this.typeCast = cast.asTypeName() } + + fun addCode(format: String, vararg args: Any?) = apply { + body.add(format, *args) + } + + fun addNamedCode(format: String, args: Map) = apply { + body.addNamed(format, args) + } + + fun addCode(codeBlock: CodeBlock) = apply { + body.add(codeBlock) + } + + /** + * Set the returnType for a generated function. + * If the type should be void you can set the type to void or ignore this option + * @param returnType the given type + */ + fun returns(returnType: TypeName) = apply { + this.returnType = returnType + } + + fun returns(returnType: ClassName) = apply { + this.returnType = returnType + } + + fun returns(returnType: Type) = apply { + this.returnType = returnType.asTypeName() + } + + fun returns(returnType: KClass<*>) = apply { + this.returnType = returnType.asClassName() + } + + fun async(async: Boolean) = apply { + this.async = async + } + + fun parameter(parameter: ParameterSpec) = apply { + check(!parameter.hasNoTypeName) { NO_PARAMETER_TYPE } + this.parameters += parameter + } + + fun parameter(parameter: () -> ParameterSpec) = apply { + check(!parameter().hasNoTypeName) { NO_PARAMETER_TYPE } + this.parameters += parameter() + } + + fun parameters(vararg parameters: ParameterSpec) = apply { + if (parameters.isEmpty()) return@apply + parameters.forEach { + check(!it.hasNoTypeName) { NO_PARAMETER_TYPE } + } + this.parameters += parameters + } + + override fun annotation(annotation: () -> AnnotationSpec) = apply { + this.specData.annotation(annotation) + } + + override fun annotation(annotation: AnnotationSpec) = apply { + this.specData.annotation(annotation) + } + + override fun annotations(vararg annotations: AnnotationSpec) = apply { + this.specData.annotations(*annotations) + } + + override fun modifier(modifier: DartModifier) = apply { + this.specData.modifier(modifier) + } + + override fun modifier(modifier: () -> DartModifier) = apply { + this.specData.modifier(modifier) + } + + override fun modifiers(vararg modifiers: DartModifier) = apply { + this.specData.modifiers += modifiers + } + + /** + * Constructs a new reference from the [FunctionSpec]. + * @return the created instance + */ + fun build(): FunctionSpec { + return FunctionSpec(this) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/function/FunctionSpec.kt b/src/main/kotlin/net/theevilreaper/dartpoet/function/FunctionSpec.kt new file mode 100644 index 00000000..f44d40cc --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/function/FunctionSpec.kt @@ -0,0 +1,115 @@ +package net.theevilreaper.dartpoet.function + +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import net.theevilreaper.dartpoet.code.CodeBlock +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.writer.FunctionWriter +import net.theevilreaper.dartpoet.code.buildCodeString +import net.theevilreaper.dartpoet.parameter.ParameterSpec +import net.theevilreaper.dartpoet.type.TypeName +import net.theevilreaper.dartpoet.util.* +import net.theevilreaper.dartpoet.util.toImmutableList +import net.theevilreaper.dartpoet.util.toImmutableSet + +/** + * The spec class contains all relevant information about a function in dart. + * A [FunctionWriter] instance read the data from it to write the data into the function structure from dart. + * @param builder the builder instance to retrieve the data from + * @author theEvilReaper + * @since 1.0.0 + * @version 1.0.0 + */ +class FunctionSpec( + builder: FunctionBuilder +) { + internal val name = builder.name + internal val returnType: TypeName? = builder.returnType + internal val body: CodeBlock = builder.body.build() + private val parameters: List = builder.parameters.toImmutableList() + internal val isAsync: Boolean = builder.async + internal val annotation: Set = builder.specData.annotations.toImmutableSet() + internal val modifiers: Set = builder.specData.modifiers.also { + hasAllowedModifiers(it, ALLOWED_FUNCTION_MODIFIERS, "function") + }.filter { it != DartModifier.PRIVATE && it != DartModifier.PUBLIC }.toImmutableSet() + internal val parametersWithDefaults = + ParameterFilter.filterParameter(parameters) { !it.isRequired && it.hasInitializer } + internal val requiredParameter = + ParameterFilter.filterParameter(parameters) { it.isRequired && !it.isNamed && !it.hasInitializer } + internal val namedParameter = ParameterFilter.filterParameter(parameters) { it.isNamed } + internal val normalParameter = + parameters.minus(parametersWithDefaults).minus(requiredParameter).minus(namedParameter).toImmutableList() + internal val hasParameters = parameters.isNotEmpty() + internal val hasAdditionalParameters = requiredParameter.isNotEmpty() || namedParameter.isNotEmpty() + + internal val isPrivate = builder.specData.modifiers.remove(DartModifier.PRIVATE) + internal val typeCast = builder.typeCast + internal val asSetter = builder.setter + internal val isGetter = builder.getter + internal val isLambda = builder.lambda + internal val docs = builder.docs + + init { + require(name.trim().isNotEmpty()) { "The name of a function can't be empty" } + require(body.isEmpty() || !modifiers.contains(DartModifier.ABSTRACT)) { "An abstract method can't have a body" } + + if (isGetter && asSetter) { + throw IllegalArgumentException("The function can't be a setter and a getter twice") + } + + if (isLambda && body.isEmpty()) { + throw IllegalArgumentException("Lambda can only be used with a body") + } + + if (requiredParameter.isNotEmpty() && parametersWithDefaults.isNotEmpty()) { + throw IllegalArgumentException("A function can't have required and optional parameters") + } + + //require (isFactory && returnType == null && !isNullable) { "A void function can't be nullable" } + } + + /** + * Calls a [FunctionWriter] to append the content from a spec object to a [CodeWriter]. + * @param codeWriter the writer instance + */ + internal fun write(codeWriter: CodeWriter) { + FunctionWriter().write(this, codeWriter) + } + + /** + * Creates a textual representation from the spec object. + * @return the spec object as string + */ + override fun toString() = buildCodeString { write(this) } + + /** + * Creates a new [FunctionBuilder] reference from an existing [FunctionSpec] object. + * @return the created [FunctionBuilder] instance + */ + fun toBuilder(): FunctionBuilder { + val builder = FunctionBuilder(this.name) + builder.returnType = this.returnType + builder.annotations(*this.annotation.toTypedArray()) + builder.modifiers(*this.modifiers.toTypedArray()) + builder.parameters.addAll(this.parameters) + builder.async = this.isAsync + builder.typeCast = this.typeCast + builder.setter = this.asSetter + builder.getter = this.isGetter + builder.lambda = this.isLambda + builder.body.formatParts.addAll(this.body.formatParts) + builder.body.args.add(this.body.args) + builder.docs.addAll(this.docs) + return builder + } + + companion object { + + /** + * Static method to create a new instance from the [FunctionBuilder]. + * @return the created instance + */ + @JvmStatic + fun builder(name: String) = FunctionBuilder(name) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/function/constructor/ConstructorBuilder.kt b/src/main/kotlin/net/theevilreaper/dartpoet/function/constructor/ConstructorBuilder.kt new file mode 100644 index 00000000..480bc000 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/function/constructor/ConstructorBuilder.kt @@ -0,0 +1,118 @@ +package net.theevilreaper.dartpoet.function.constructor + +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.code.CodeBlock +import net.theevilreaper.dartpoet.parameter.ParameterSpec + +class ConstructorBuilder( + val name: String, + val named: String? = null, + vararg modifiers: DartModifier +) { + internal val parameters: MutableList = mutableListOf() + internal var lambda: Boolean = false + internal val initializer: CodeBlock.Builder = CodeBlock.builder() + internal var factory: Boolean = false + internal val modifiers: MutableList = mutableListOf(*modifiers) + internal val docs: MutableList = mutableListOf() + + /** + * Add a comment over for the extension class. + * Note this comments will be generated over the extension class + * @param format the string which contains the content and the format + * @param args the arguments for the format string + */ + fun doc(format: String, vararg args: Any) = apply { + this.docs.add(CodeBlock.of(format.replace(' ', '·'), *args)) + } + + /** + * Add a [DartModifier] to the constructor. + * @param modifier the modifier to add + */ + fun modifier(modifier: DartModifier) = apply { + this.modifiers += modifier + } + + /** + * Add an array of [DartModifier] to the constructor. + * @param modifiers the modifiers to add + */ + fun modifiers(vararg modifiers: DartModifier) = apply { + this.modifiers += modifiers + } + + /** + * Indicates if the constructor should be generated as factory. + * @param factory True for a factory generation otherwise false + */ + fun asFactory(factory: Boolean) = apply { + this.factory = factory + } + + /** + * Add a format string with arguments as initializer. + * @param format the format for the block + * @param args the arguments for the format + */ + fun addCode(format: String, vararg args: Any?) = apply { + initializer.add(format, *args) + } + + /** + * Add a format string with arguments as initializer. + * @param format the format for the block + * @param args the arguments for the format + */ + fun addNamedCode(format: String, args: Map) = apply { + initializer.addNamed(format, args) + } + + /** + * Add a [CodeBlock] which contains the structure for the initializer for a constructor. + * @param codeBlock the block to add + */ + fun addCode(codeBlock: CodeBlock) = apply { + initializer.add(codeBlock) + } + + /** + * Indicates if the constructor should be generated with lambda. + * @param lambda True for a lambda variant otherwise false + */ + fun lambda(lambda: Boolean) = apply { + this.lambda = lambda + } + + /** + * Add a [ParameterSpec] to the builder. + * @param parameterSpec the parameter to add + */ + fun parameter(parameterSpec: ParameterSpec) = apply { + this.parameters += parameterSpec + } + + /** + * Add a [ParameterSpec] to the builder. + * @param parameterSpec the parameter to add + */ + fun parameter(parameterSpec: () -> ParameterSpec) = apply { + this.parameters += parameterSpec() + } + + /** + * Add an array of [ParameterSpec] to the builder. + * @param parameterSpec an array of parameters + */ + fun parameters(vararg parameterSpec: ParameterSpec) = apply { + this.parameters += parameterSpec + } + + /** + * Creates a new object reference from the [ConstructorSpec]. + * @return the created reference + */ + fun build(): ConstructorSpec { + return ConstructorSpec(this) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/function/constructor/ConstructorSpec.kt b/src/main/kotlin/net/theevilreaper/dartpoet/function/constructor/ConstructorSpec.kt new file mode 100644 index 00000000..b39fe07f --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/function/constructor/ConstructorSpec.kt @@ -0,0 +1,64 @@ +package net.theevilreaper.dartpoet.function.constructor + +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.buildCodeString +import net.theevilreaper.dartpoet.code.writer.ConstructorWriter +import net.theevilreaper.dartpoet.util.toImmutableList +import net.theevilreaper.dartpoet.util.toImmutableSet + +class ConstructorSpec( + builder: ConstructorBuilder +) { + + internal val name = builder.name + internal val named = builder.named + internal val isNamed = named.orEmpty().trim().isNotEmpty() + internal val isLambda = builder.lambda + internal val isFactory = builder.factory + internal val initializer = builder.initializer + internal val modifiers = builder.modifiers.toImmutableSet() + private val modelParameters = builder.parameters.toImmutableSet() + internal val requiredAndNamedParameters = + builder.parameters.filter { it.isRequired || it.isNamed }.toImmutableList() + internal val parameters = modelParameters.minus(requiredAndNamedParameters.toSet()).toImmutableList() + internal val hasParameters = builder.parameters.isNotEmpty() + internal val hasNamedParameters = requiredAndNamedParameters.isNotEmpty() + internal val docs = builder.docs.toImmutableList() + + internal fun write( + codeWriter: CodeWriter + ) { + ConstructorWriter().write(this, codeWriter) + } + + override fun toString() = buildCodeString { write(this,) } + + /** + * Creates a new [ConstructorBuilder] reference from an existing [ConstructorSpec] object. + * @return the created [ConstructorBuilder] instance + */ + fun toBuilder(): ConstructorBuilder { + val builder = ConstructorBuilder(this.name, this.named) + builder.lambda = this.isLambda + builder.factory = this.isFactory + builder.modifiers.addAll(this.modifiers) + builder.parameters.addAll(this.modelParameters) + + if (this.initializer.build().isNotEmpty()) { + builder.initializer.formatParts.addAll(this.initializer.formatParts) + builder.initializer.args.addAll(this.initializer.args) + } + + builder.docs.addAll(this.docs) + return builder + } + + companion object { + + @JvmStatic + fun builder(name: String) = ConstructorBuilder(name) + + @JvmStatic + fun named(name: String, methodName: String) = ConstructorBuilder(name, methodName) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/function/typedef/TypeDefBuilder.kt b/src/main/kotlin/net/theevilreaper/dartpoet/function/typedef/TypeDefBuilder.kt new file mode 100644 index 00000000..13a5b5aa --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/function/typedef/TypeDefBuilder.kt @@ -0,0 +1,103 @@ +package net.theevilreaper.dartpoet.function.typedef + +import net.theevilreaper.dartpoet.parameter.ParameterSpec +import net.theevilreaper.dartpoet.type.ClassName +import net.theevilreaper.dartpoet.type.TypeName +import net.theevilreaper.dartpoet.type.asTypeName +import kotlin.reflect.KClass + +/** + * The builder is used to create a type definition with a specific name and optional type cast. + * After the construction the builder maps the data into a [TypeDefSpec] object. + * + * @param typeDefName the name of the type definition. + * @param typeCasts optional array of type-cast for the type definition. + */ +class TypeDefBuilder internal constructor( + val typeDefName: String, + vararg val typeCasts: TypeName? = emptyArray() +) { + /** + * The name of the type definition. + */ + var name: String? = null + + /** + * The return type of the type definition. + */ + var returnType: TypeName? = null + + /** + * List of parameters associated with the type definition. + */ + val parameters: MutableList = mutableListOf() + + /** + * Sets the name of the type definition. + * + * @param name the name of the type definition. + * @return the current instance of [TypeDefBuilder]. + */ + fun name(name: String) = apply { + this.name = name + } + + /** + * Adds a parameter to the list of parameters. + * + * @param parameterSpec the parameter specification. + * @return the current instance of [TypeDefBuilder]. + */ + fun parameter(parameterSpec: ParameterSpec) = apply { + this.parameters += parameterSpec + } + + /** + * Adds multiple parameters to the list of parameters. + * + * @param parameterSpecs the parameter specifications. + * @return the current instance of [TypeDefBuilder]. + */ + fun parameters(vararg parameterSpecs: ParameterSpec) = apply { + this.parameters += parameterSpecs + } + + /** + * Sets the return type of the type definition. + * + * @param typeName the return type as a [TypeName]. + * @return the current instance of [TypeDefBuilder]. + */ + fun returns(typeName: TypeName) = apply { + this.returnType = typeName + } + + /** + * Sets the return type of the type definition. + * + * @param typeName the return type as a [ClassName]. + * @return the current instance of [TypeDefBuilder]. + */ + fun returns(typeName: ClassName) = apply { + this.returnType = typeName + } + + /** + * Sets the return type of the type definition using a [KClass]. + * + * @param typeName the return type as a [KClass]. + * @return the current instance of [TypeDefBuilder]. + */ + fun returns(typeName: KClass<*>) = apply { + this.returnType = typeName.asTypeName() + } + + /** + * Builds and returns an instance of [TypeDefSpec] based on the configuration. + * + * @return an instance of [TypeDefSpec]. + */ + fun build(): TypeDefSpec { + return TypeDefSpec(this) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/function/typedef/TypeDefSpec.kt b/src/main/kotlin/net/theevilreaper/dartpoet/function/typedef/TypeDefSpec.kt new file mode 100644 index 00000000..26ae1bb2 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/function/typedef/TypeDefSpec.kt @@ -0,0 +1,124 @@ +package net.theevilreaper.dartpoet.function.typedef + +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.buildCodeString +import net.theevilreaper.dartpoet.code.writer.FunctionWriter +import net.theevilreaper.dartpoet.code.writer.TypeDefWriter +import net.theevilreaper.dartpoet.type.ClassName +import net.theevilreaper.dartpoet.type.TypeName +import net.theevilreaper.dartpoet.type.asTypeName +import net.theevilreaper.dartpoet.util.ParameterFilter +import net.theevilreaper.dartpoet.util.toImmutableList +import kotlin.reflect.KClass + +/** + * The class models a typedef from dart into a structure which can be used to generate and organize such methods. + * For more details visit the documentation from dart + * @param builder the builder instance to retrieve the data from + * @see Dart Typedefs. + */ +class TypeDefSpec( + val builder: TypeDefBuilder +) { + internal val typeDefName = builder.typeDefName + internal val name = builder.name + internal val typeCasts = builder.typeCasts + internal val returnType = builder.returnType ?: Void::class.asTypeName() + internal val parameters = builder.parameters.toImmutableList() + internal val parametersWithDefaults = + ParameterFilter.filterParameter(parameters) { !it.isRequired && it.hasInitializer } + internal val requiredParameter = + ParameterFilter.filterParameter(parameters) { it.isRequired && !it.isNamed && !it.hasInitializer } + internal val namedParameter = ParameterFilter.filterParameter(parameters) { it.isNamed } + internal val normalParameter = + parameters.minus(parametersWithDefaults).minus(requiredParameter).minus(namedParameter).toImmutableList() + internal val hasAdditionalParameters = requiredParameter.isNotEmpty() || namedParameter.isNotEmpty() + internal val hasParameters = normalParameter.isNotEmpty() + + /** + * Performs some checks to avoid invalid data. + */ + init { + require(typeDefName.trim().isNotEmpty()) { "The name of a typedef can't be empty" } + if (name != null) { + require(name.trim().isNotEmpty()) { "The function name of a typedef can't be empty" } + } + } + + /** + * Calls a [FunctionWriter] to append the content from a spec object to a [CodeWriter]. + * @param codeWriter the writer instance + */ + internal fun write(codeWriter: CodeWriter) { + TypeDefWriter().write(this, codeWriter) + } + + /** + * Creates a textual representation from the spec object. + * @return the spec object as string + */ + override fun toString() = buildCodeString { write(this) } + + /** + * Converts a given instance of a [TypeDefSpec] into a [TypeDefBuilder]. + * This is useful if you want to modify an existing spec object. + * @return the created builder + */ + fun toBuilder(): TypeDefBuilder { + return builder + } + + /** + * The companion object contains some helper methods to create a new instance of a [TypeDefSpec]. + */ + companion object { + + /** + * Static method to create a new instance from the [TypeDefBuilder]. + * @param typeDefName the name of the typedef + * @return the created instance + */ + @JvmStatic + fun builder(typeDefName: String): TypeDefBuilder = TypeDefBuilder(typeDefName) + + /** + * Static method to create a new instance from the [TypeDefBuilder]. + * @param typeDefName the name of the typedef + * @param typeCasts the type cast for the typedef as [TypeName] + * @return the created instance + */ + @JvmStatic + fun builder(typeDefName: String, vararg typeCasts: TypeName): TypeDefBuilder = + TypeDefBuilder(typeDefName, *typeCasts) + + /** + * Static method to create a new instance from the [TypeDefBuilder]. + * @param typeDefName the name of the typedef + * @param typeCast the type cast for the typedef as [Class] + * @return the created instance + */ + @JvmStatic + fun builder(typeDefName: String, vararg typeCasts: ClassName): TypeDefBuilder = + TypeDefBuilder(typeDefName, *typeCasts) + + /** + * Static method to create a new instance from the [TypeDefBuilder]. + * @param typeDefName the name of the typedef + * @param typeCasts the type cast for the typedef as [Class] + * @return the created instance + */ + @JvmStatic + fun builder(typeDefName: String, vararg typeCasts: Class<*>): TypeDefBuilder = + TypeDefBuilder(typeDefName, *typeCasts.map { it.asTypeName() }.toTypedArray()) + + /** + * Static method to create a new instance from the [TypeDefBuilder]. + * @param typeDefName the name of the typedef + * @param typeCasts the type cast for the typedef as [KClass] + * @return the created instance + */ + @JvmStatic + fun builder(typeDefName: String, vararg typeCasts: KClass<*>): TypeDefBuilder = + TypeDefBuilder(typeDefName, *typeCasts.map { it.asTypeName() }.toTypedArray()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/import/DartImport.kt b/src/main/kotlin/net/theevilreaper/dartpoet/import/DartImport.kt deleted file mode 100644 index 7d2da2a9..00000000 --- a/src/main/kotlin/net/theevilreaper/dartpoet/import/DartImport.kt +++ /dev/null @@ -1,56 +0,0 @@ -package net.theevilreaper.dartpoet.import - -import net.theevilreaper.dartpoet.util.IMPORT -import java.lang.IllegalStateException - -/** - * The class represents a normal import from dart. - * An import statement can look like this in dart: - *
    - *
  1. 'package:flutter/material.dart'
  2. - *
  3. '../../model/item_model.dart'
  4. - *
- * Dart also allows to add a prefix to an import which means that an import can look like that: - * -> import ../../model/item_model.dart as itemModel; - * - * @author theEvilReaper - * @since 1.0.0 - */ -class DartImport internal constructor( - private val path: String, - private val importCastType: ImportCastType? = null, - private val importCast: String? = null -) : Import { - - private val importString = buildString { - append("$IMPORT ") - if (importCast == null && importCastType == null) { - append("'package:$path';") - } else if (importCast != null && importCastType != null) { - append("'$path' ${importCastType.identifier} $importCast;") - } else { - throw IllegalStateException("NOPE") - } - } - - /** - * Checks if the importCast string is null or empty. - * It used to determine if the import statement should include the 'as name' in the generation - * @return true when the string is null or empty otherwise false - */ - private fun includePrefix(): Boolean { - return !importCast.isNullOrEmpty() - } - - /** - * Checks if the given name starts with a dot. - * @return true when the import starts with a dot otherwise false - */ - private fun startWithDot(): Boolean { - return this.path.startsWith(".") - } - - override fun compareTo(other: Import): Int = importString.compareTo(other.toString()) - - override fun toString(): String = importString -} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/import/Import.kt b/src/main/kotlin/net/theevilreaper/dartpoet/import/Import.kt deleted file mode 100644 index a5424475..00000000 --- a/src/main/kotlin/net/theevilreaper/dartpoet/import/Import.kt +++ /dev/null @@ -1,3 +0,0 @@ -package net.theevilreaper.dartpoet.import - -sealed interface Import: Comparable \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/import/PartImport.kt b/src/main/kotlin/net/theevilreaper/dartpoet/import/PartImport.kt deleted file mode 100644 index 0a4fb5b0..00000000 --- a/src/main/kotlin/net/theevilreaper/dartpoet/import/PartImport.kt +++ /dev/null @@ -1,17 +0,0 @@ -package net.theevilreaper.dartpoet.import - -class PartImport internal constructor( - private val path: String -): Import { - - private val partImport: String = buildString { - append("part ") - append("'") - append(path) - append("';") - } - - override fun toString(): String = partImport - - override fun compareTo(other: Import): Int = partImport.compareTo(other.toString()) -} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/meta/SpecData.kt b/src/main/kotlin/net/theevilreaper/dartpoet/meta/SpecData.kt index 111ac51c..033883c0 100644 --- a/src/main/kotlin/net/theevilreaper/dartpoet/meta/SpecData.kt +++ b/src/main/kotlin/net/theevilreaper/dartpoet/meta/SpecData.kt @@ -3,35 +3,17 @@ package net.theevilreaper.dartpoet.meta import net.theevilreaper.dartpoet.DartModifier import net.theevilreaper.dartpoet.annotation.AnnotationSpec -class SpecData : SpecMethods { +/** + * The class is the implementation of the [SpecMethods] interface. + * It accepts any type for the generic return value from the given interface + * @author theEvilReaper + * @since 1.0.0 + */ +class SpecData(vararg modifiers: DartModifier = emptyArray()) : SpecMethods { - internal val modifiers: MutableList = mutableListOf() + internal val modifiers: MutableSet = mutableSetOf(*modifiers) internal val annotations: MutableList = mutableListOf() - /** - * Add a [Iterable] of [DartModifier]. - * @param modifiers the modifiers to add - */ - override fun modifiers(modifiers: Iterable) { - this.modifiers += modifiers; - } - - /** - * Add a [Iterable] of [AnnotationSpec]. - * @param annotations the annotations to add - */ - override fun annotations(annotations: Iterable) { - this.annotations += annotations - } - - /** - * Add a [Iterable] of [AnnotationSpec]. - * @param annotations the annotations to add - */ - override fun annotations(annotations: () -> Iterable) { - this.annotations += annotations() - } - /** * Add a single [AnnotationSpec]. * @param annotation the annotation to add @@ -48,6 +30,14 @@ class SpecData : SpecMethods { this.annotations += annotation } + /** + * Add an array of [AnnotationSpec] to the underlying set. + * @param annotations the array with the annotations + */ + override fun annotations(vararg annotations: AnnotationSpec) { + this.annotations += annotations + } + /** * Add a new [DartModifier]. * @param modifier the modifier to add @@ -65,10 +55,10 @@ class SpecData : SpecMethods { } /** - * Add a [Iterable] of [DartModifier]. - * @param modifiers the modifiers to add + * Add an array of [DartModifier] to the given [MutableSet]. + * @param modifiers the array with the modifiers */ - override fun modifiers(modifiers: () -> Iterable) { - this.modifiers += modifiers() + override fun modifiers(vararg modifiers: DartModifier) { + this.modifiers += modifiers } -} \ No newline at end of file +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/meta/SpecMethods.kt b/src/main/kotlin/net/theevilreaper/dartpoet/meta/SpecMethods.kt index 26d7b158..4c3aebbb 100644 --- a/src/main/kotlin/net/theevilreaper/dartpoet/meta/SpecMethods.kt +++ b/src/main/kotlin/net/theevilreaper/dartpoet/meta/SpecMethods.kt @@ -10,18 +10,6 @@ import net.theevilreaper.dartpoet.annotation.AnnotationSpec */ internal interface SpecMethods { - /** - * Add a [Iterable] of [AnnotationSpec]. - * @param annotations the annotations to add - */ - fun annotations(annotations: Iterable): T - - /** - * Add a [Iterable] of [AnnotationSpec]. - * @param annotations the annotations to add - */ - fun annotations(annotations: () -> Iterable): T - /** * Add a single [AnnotationSpec]. * @param annotation the annotation to add @@ -34,6 +22,12 @@ internal interface SpecMethods { */ fun annotation(annotation: AnnotationSpec): T + /** + * Add an array of [AnnotationSpec]. + * @param annotations the annotations to add + */ + fun annotations(vararg annotations: AnnotationSpec): T + /** * Add a new [DartModifier]. * @param modifier the modifier to add @@ -47,14 +41,8 @@ internal interface SpecMethods { fun modifier(modifier: () -> DartModifier): T /** - * Add a [Iterable] of [DartModifier]. - * @param modifiers the modifiers to add - */ - fun modifiers(modifiers: Iterable): T - - /** - * Add a [Iterable] of [DartModifier]. + * Add a variable number of [DartModifier]'s. * @param modifiers the modifiers to add */ - fun modifiers(modifiers: () -> Iterable): T -} \ No newline at end of file + fun modifiers(vararg modifiers: DartModifier): T +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/method/DartMethodBuilder.kt b/src/main/kotlin/net/theevilreaper/dartpoet/method/DartMethodBuilder.kt deleted file mode 100644 index 2de33956..00000000 --- a/src/main/kotlin/net/theevilreaper/dartpoet/method/DartMethodBuilder.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.theevilreaper.dartpoet.method - -class DartMethodBuilder internal constructor( - name: String -) { -} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/method/DartMethodSpec.kt b/src/main/kotlin/net/theevilreaper/dartpoet/method/DartMethodSpec.kt deleted file mode 100644 index 989f6ba9..00000000 --- a/src/main/kotlin/net/theevilreaper/dartpoet/method/DartMethodSpec.kt +++ /dev/null @@ -1,19 +0,0 @@ -package net.theevilreaper.dartpoet.method - -class DartMethodSpec( - builder: DartMethodBuilder -) { - - companion object { - - @JvmStatic fun builder(name: String) = DartMethodBuilder(name) - - @JvmStatic fun constructor(name: String, const: Boolean) { - - } - - @JvmStatic fun namedConstructor(name: String) { - - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/parameter/ParameterBuilder.kt b/src/main/kotlin/net/theevilreaper/dartpoet/parameter/ParameterBuilder.kt new file mode 100644 index 00000000..a8e2a603 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/parameter/ParameterBuilder.kt @@ -0,0 +1,100 @@ +package net.theevilreaper.dartpoet.parameter + +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import net.theevilreaper.dartpoet.code.CodeBlock +import net.theevilreaper.dartpoet.meta.SpecData +import net.theevilreaper.dartpoet.meta.SpecMethods +import net.theevilreaper.dartpoet.type.TypeName + +/** + * [ParameterBuilder] is responsible for configuring and assembling details of a parameter, such as its name, + * type, named status, required status, nullability, and initializer. It implements the [SpecMethods] + * interface to provide methods for customizing parameter specifications. + * + * This class is typically used in code generation tasks to construct and customize parameter specifications + * before creating instances of [ParameterSpec]. + * + * @param name The name of the parameter + * @param typeName The type of the parameter, represented as a [TypeName] + * @author theEvilReaper + * @since 1.0.0 + */ +class ParameterBuilder internal constructor( + val name: String, + val typeName: TypeName?, +) : SpecMethods { + internal val specData: SpecData = SpecData() + internal var named: Boolean = false + internal var nullable: Boolean = false + internal var initializer: CodeBlock? = null + + fun initializer(format: String, vararg args: Any) = apply { + initializer(CodeBlock.of(format, *args)) + } + + fun initializer(block: CodeBlock) = apply { + this.initializer = block + } + + fun named(named: Boolean) = apply { + this.named = named + } + + /** + * Indicates whether the parameter is nullable or not. + * @param nullable true if the parameter is nullable, false otherwise + * @return the current [ParameterBuilder] instance + */ + fun nullable(nullable: Boolean) = apply { + this.nullable = nullable + } + + /** + * Indicates that the parameter is required. + * @return the current [ParameterBuilder] instance + */ + fun required() = apply { + this.modifiers(DartModifier.REQUIRED) + } + + override fun annotation(annotation: () -> AnnotationSpec) = apply { + this.specData.annotations += annotation() + } + + override fun annotation(annotation: AnnotationSpec) = apply { + this.specData.annotations += annotation + } + + override fun annotations(vararg annotations: AnnotationSpec) = apply { + this.specData.annotations(*annotations) + } + + override fun modifier(modifier: DartModifier) = apply { + this.specData.modifiers += modifier + } + + override fun modifier(modifier: () -> DartModifier) = apply { + this.specData.modifiers += modifier() + } + + /** + * Add a given array of [DartModifier] to the builder instance. + * @param modifiers the array to add + * @return the given instance from the builder + */ + override fun modifiers(vararg modifiers: DartModifier) = apply { + this.specData.modifiers += modifiers + } + + /** + * This method constructs a [ParameterSpec] object using the settings and data defined in the associated + * [ParameterBuilder]. It is typically used to create a parameter specification after configuring it + * with the desired parameter details. + * + * @return a [ParameterSpec] instance representing the parameter specification + */ + fun build(): ParameterSpec { + return ParameterSpec(this) + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/parameter/ParameterSpec.kt b/src/main/kotlin/net/theevilreaper/dartpoet/parameter/ParameterSpec.kt new file mode 100644 index 00000000..0a9d31be --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/parameter/ParameterSpec.kt @@ -0,0 +1,120 @@ +package net.theevilreaper.dartpoet.parameter + +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.buildCodeString +import net.theevilreaper.dartpoet.code.writer.ParameterWriter +import net.theevilreaper.dartpoet.type.ClassName +import net.theevilreaper.dartpoet.type.TypeName +import net.theevilreaper.dartpoet.type.asClassName +import net.theevilreaper.dartpoet.util.toImmutableSet +import kotlin.reflect.KClass + +/** + * Represents a parameter specification used in code generation. + * + * [ParameterSpec] encapsulates information about a parameter, including its name, type, whether it is named, + * nullable, required, initializer, and associated annotations. It is used to generate code constructs + * that involve method or function parameters. + * + * This class is typically used in code generation tasks to define and manipulate parameter specifications + * for generating source code. + * @param builder the builder instance to retrieve the data from + * @author theEvilReaper + * @since 1.0.0 + */ +class ParameterSpec internal constructor( + builder: ParameterBuilder +) { + internal val name = builder.name + internal val type = builder.typeName + internal val isNamed = builder.named + internal val isNullable = builder.nullable + internal val isRequired = builder.specData.modifiers.contains(DartModifier.REQUIRED) + internal val initializer = builder.initializer + internal val annotations = builder.specData.annotations.toImmutableSet() + internal val hasInitializer = initializer != null && initializer.isNotEmpty() + internal val hasNoTypeName: Boolean = builder.typeName == null + + /** + * This init block is responsible for performing initial checks on the name parameter. + * It ensures that the given name is not empty by trimming it and checking for non-empty content. + */ + init { + check(name.trim().isNotEmpty()) { "The name of a parameter can't be empty" } + } + + /** + * This method delegates the writing process to a [ParameterWriter] instance, which is responsible for + * writing the parameter details to the specified [CodeWriter]. + * + * @param codeWriter the [CodeWriter] to which the parameter should be written + */ + internal fun write(codeWriter: CodeWriter) { + ParameterWriter().write(this, codeWriter) + } + + /** + * Returns a textual representation of the given [ParameterSpec] instance. + * @return the created representation as [String] + */ + override fun toString() = buildCodeString { write(this) } + + /** + * Creates a new [ParameterBuilder] reference from an existing [ParameterSpec] object. + * @return the created [ParameterBuilder] instance + */ + fun toBuilder(): ParameterBuilder { + val builder = ParameterBuilder(this.name, this.type) + builder.named = isNamed + builder.nullable = isNullable + builder.annotations(*this.annotations.toTypedArray()) + if (isRequired) { + builder.modifiers(DartModifier.REQUIRED) + } + builder.initializer = initializer + return builder + } + + companion object { + + /** + * Creates a new instance of [ParameterBuilder] with the specified name and type. + * + * @param name the name for the parameter. Should adhere to naming conventions + * @param type the type for the parameter, represented as a [TypeName] + * @return A new [ParameterBuilder] instance initialized with the provided name and type + */ + @JvmStatic + fun builder(name: String, type: TypeName) = ParameterBuilder(name, type) + + /** + * Creates a new instance of [ParameterBuilder] with the specified name and type. + * + * @param name The name for the parameter. Should adhere to naming conventions + * @param type the type for the parameter, represented as a [KClass] + * @return A new [ParameterBuilder] instance initialized with the provided name and type + */ + @JvmStatic + fun builder(name: String, type: KClass<*>) = ParameterBuilder(name, type.asClassName()) + + /** + * Creates a new instance of [ParameterBuilder] with the specified name and type. + * + * @param name the name for the parameter. Should adhere to naming conventions + * @param className the type for the parameter, represented as a [ClassName] + * @return A new [ParameterBuilder] instance initialized with the provided name and type + */ + @JvmStatic + fun builder(name: String, className: ClassName) = ParameterBuilder(name, className) + + /** + * Creates a new instance of [ParameterBuilder] with the specified name and type. + * + * @param name the name for the parameter. Should adhere to naming conventions + * @return A new [ParameterBuilder] instance initialized with the provided name and type + */ + @JvmStatic + fun builder(name: String) = ParameterBuilder(name, null) + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/property/DartPropertyBuilder.kt b/src/main/kotlin/net/theevilreaper/dartpoet/property/DartPropertyBuilder.kt deleted file mode 100644 index a9c3dc17..00000000 --- a/src/main/kotlin/net/theevilreaper/dartpoet/property/DartPropertyBuilder.kt +++ /dev/null @@ -1,111 +0,0 @@ -package net.theevilreaper.dartpoet.property - -import net.theevilreaper.dartpoet.DartModifier -import net.theevilreaper.dartpoet.annotation.AnnotationSpec -import net.theevilreaper.dartpoet.code.CodeFragment -import net.theevilreaper.dartpoet.code.CodeFragmentBuilder - - -/** - * @author theEvilReaper - * @version 1.0.0 - * @since - **/ - -class DartPropertyBuilder internal constructor( - var name: String, - var type: String, - vararg modifiers: DartModifier -) { - - internal var nullable = false - internal val modifiers: MutableList = mutableListOf() - internal val annotations: MutableList = mutableListOf() - internal var initBlock: CodeFragment? = null - - fun initWith(format: String, vararg args: Any?): DartPropertyBuilder = apply { - this.initWith(CodeFragmentBuilder.of(format, args)) - } - - fun initWith(codeFragment: CodeFragment?): DartPropertyBuilder = apply { - this.initBlock = codeFragment - } - - /** - * Set if the property should be nullable or not. A property which is nullable in Dart contains a '?' after the type. - * @param nullable if the property should be nullable or not - */ - fun nullable(nullable: Boolean): DartPropertyBuilder { - this.nullable = nullable - return this - } - - /** - * Add a [Iterable] of [AnnotationSpec] to the property. - * @param annotations the annotations to add - */ - fun annotations(annotations: Iterable): DartPropertyBuilder = apply { - this.annotations += annotations - } - - /** - * Add a [Iterable] of [AnnotationSpec] to the property. - * @param annotations the annotations to add - */ - fun annotations(annotations: () -> Iterable): DartPropertyBuilder = apply { - this.annotations += annotations() - } - - /** - * Add a single [AnnotationSpec] to the property. - * @param annotation the annotation to add - */ - fun annotation(annotation: () -> AnnotationSpec): DartPropertyBuilder = apply { - this.annotations += annotation() - } - - /** - * Add a single [AnnotationSpec] to the property. - * @param annotation the annotation to add - */ - fun annotation(annotation: AnnotationSpec): DartPropertyBuilder = apply { - this.annotations += annotation - } - - /** - * Add a new [DartModifier] to the property. - * @param modifier the modifier to add - */ - fun modifier(modifier: DartModifier): DartPropertyBuilder = apply { - this.modifiers += modifier - } - - /** - * Add a new [DartModifier] to the property. - * @param modifier the modifier to add - */ - fun modifier(modifier: () -> DartModifier): DartPropertyBuilder = apply { - this.modifiers += modifier() - } - - /** - * Add a [Iterable] of [DartModifier] to the property. - * @param modifiers the modifiers to add - */ - fun modifiers(modifiers: Iterable): DartPropertyBuilder = apply { - this.modifiers += modifiers; - } - - /** - * Add a [Iterable] of [DartModifier] to the property. - * @param modifiers the modifiers to add - */ - fun modifiers(modifiers: () -> Iterable): DartPropertyBuilder = apply { - this.modifiers += modifiers() - } - - fun build(): DartPropertySpec { - return DartPropertySpec(this) - } - -} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/property/DartPropertySpec.kt b/src/main/kotlin/net/theevilreaper/dartpoet/property/DartPropertySpec.kt deleted file mode 100644 index b163a843..00000000 --- a/src/main/kotlin/net/theevilreaper/dartpoet/property/DartPropertySpec.kt +++ /dev/null @@ -1,37 +0,0 @@ -package net.theevilreaper.dartpoet.property - -import net.theevilreaper.dartpoet.DartModifier -import net.theevilreaper.dartpoet.annotation.AnnotationSpec -import net.theevilreaper.dartpoet.util.toImmutableSet -import net.theevilreaper.dartpoet.writer.CodeWriter - -class DartPropertySpec( - builder: DartPropertyBuilder -) { - - private var name = builder.name - private var type = builder.type - private var modifiers: Set = builder.modifiers.toImmutableSet() - private var annotations: Set = builder.annotations.toImmutableSet() - private var nullable = builder.nullable - private var initBlock = builder.initBlock - - internal fun write( - codeWriter: CodeWriter, - implicitModifiers: Set, - withInitializer: Boolean = true, - ) { - } - - companion object { - - @JvmStatic - fun builder( - name: String, - type: String, - vararg modifiers: DartModifier - ): DartPropertyBuilder { - return DartPropertyBuilder(name, type, *modifiers) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/property/PropertyBuilder.kt b/src/main/kotlin/net/theevilreaper/dartpoet/property/PropertyBuilder.kt new file mode 100644 index 00000000..cf5c6238 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/property/PropertyBuilder.kt @@ -0,0 +1,99 @@ +package net.theevilreaper.dartpoet.property + +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import net.theevilreaper.dartpoet.code.CodeBlock +import net.theevilreaper.dartpoet.type.TypeName + +/** + * The builder is used to set all values that describe a property in Dart. + * @param name the name of the property + * @param type the type as [TypeName] of the property (can be nullable) + * @author theEvilReaper + * @version 1.0.0 + * @since 1.0.0 + **/ +class PropertyBuilder internal constructor( + var name: String, + var type: TypeName? = null, +) { + internal val modifiers: MutableSet = mutableSetOf() + internal val annotations: MutableList = mutableListOf() + internal var initBlock: CodeBlock.Builder = CodeBlock.builder() + internal val docs: MutableList = mutableListOf() + + /** + * Add a comment over for the extension class. + * Note this comments will be generated over the extension class + * @param format the string which contains the content and the format + * @param args the arguments for the format string + */ + fun docs(format: String, vararg args: Any) = apply { + this.docs.add(CodeBlock.of(format.replace(' ', '·'), *args)) + } + + /** + * Apply a given format which contains the parts for the init block of the [PropertySpec]. + * @param format the given format + * @param args the arguments for the format + */ + fun initWith(format: String, vararg args: Any?): PropertyBuilder = apply { + this.initBlock.add(format, *args) + } + + /** + * Set the initializer block directly as [CodeBlock.Builder] to the property. + * @param codeFragment the [CodeBlock.Builder] to set + */ + fun initWith(codeFragment: CodeBlock.Builder): PropertyBuilder = apply { + this.initBlock = codeFragment + } + + /** + * Add a single [AnnotationSpec] to the property. + * @param annotation the annotation to add + */ + fun annotation(annotation: () -> AnnotationSpec): PropertyBuilder = apply { + this.annotations += annotation() + } + + /** + * Add a single [AnnotationSpec] to the property. + * @param annotation the annotation to add + */ + fun annotation(annotation: AnnotationSpec): PropertyBuilder = apply { + this.annotations += annotation + } + + /** + * Add a new [DartModifier] to the property. + * @param modifier the modifier to add + */ + fun modifier(modifier: DartModifier): PropertyBuilder = apply { + this.modifiers += modifier + } + + /** + * Add a new [DartModifier] to the property. + * @param modifier the modifier to add + */ + fun modifier(modifier: () -> DartModifier): PropertyBuilder = apply { + this.modifiers += modifier() + } + + /** + * Add an [Array] of [DartModifier]'s to the property. + * @param modifiers the modifier values to add + */ + fun modifiers(vararg modifiers: DartModifier) = apply { + this.modifiers += modifiers + } + + /** + * Creates a new reference from the [PropertySpec] with the given builder reference. + * @return the created [PropertySpec] instance + */ + fun build(): PropertySpec { + return PropertySpec(this) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/property/PropertySpec.kt b/src/main/kotlin/net/theevilreaper/dartpoet/property/PropertySpec.kt new file mode 100644 index 00000000..ecb52a1d --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/property/PropertySpec.kt @@ -0,0 +1,175 @@ +package net.theevilreaper.dartpoet.property + +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.writer.PropertyWriter +import net.theevilreaper.dartpoet.code.buildCodeString +import net.theevilreaper.dartpoet.type.ClassName +import net.theevilreaper.dartpoet.type.TypeName +import net.theevilreaper.dartpoet.type.asTypeName +import net.theevilreaper.dartpoet.util.ALLOWED_CONST_MODIFIERS +import net.theevilreaper.dartpoet.util.toImmutableSet +import net.theevilreaper.dartpoet.util.ALLOWED_PROPERTY_MODIFIERS +import net.theevilreaper.dartpoet.util.hasAllowedModifiers +import kotlin.reflect.KClass + +/** + * The property spec class contains all variables which are comes from the [PropertyBuilder]. + * Some values are checked for certain conditions to avoid errors during the generation. + * @param builder the builder instance to retrieve the data from + * @author theEvilReaper + * @since 1.0.0 + */ +class PropertySpec( + builder: PropertyBuilder +) { + internal var name = builder.name + internal var type = builder.type + internal var annotations: Set = builder.annotations.toImmutableSet() + internal var initBlock = builder.initBlock + internal var isPrivate = builder.modifiers.contains(DartModifier.PRIVATE) + internal var isConst = builder.modifiers.contains(DartModifier.CONST) + internal val docs = builder.docs + internal var modifiers: Set = builder.modifiers + .also { + if (it.isNotEmpty()) { + hasAllowedModifiers(it, ALLOWED_PROPERTY_MODIFIERS, "property") + if (type == null) { + hasAllowedModifiers(it, ALLOWED_CONST_MODIFIERS, "const property") + it.clear() + it.addAll(ALLOWED_CONST_MODIFIERS) + } + } + }.filter { it != DartModifier.PRIVATE && it != DartModifier.PUBLIC }.toImmutableSet() + internal val hasModifiers: Boolean = modifiers.isNotEmpty() + init { + require(name.trim().isNotEmpty()) { "The name of a property can't be empty" } + + if (builder.type == null && !isConst) { + throw IllegalArgumentException("Only a const property can have no type") + } + + if (isConst && this.initBlock.isEmpty()) { + throw IllegalArgumentException("A const variable needs an init block") + } + } + + /** + * Trigger the write process from the [PropertyWriter] to write the spec into dart code. + * @param codeWriter the [CodeWriter] to apply the content from the spec + */ + internal fun write(codeWriter: CodeWriter) { + PropertyWriter().write(this, codeWriter) + } + + /** + * Returns a textual representation of the spec class. + * It calls the [write] method to get the representation + * @return the created representation + */ + override fun toString() = buildCodeString { write(this) } + + /** + * Creates a new [PropertyBuilder] reference from an existing [PropertySpec] object. + * @return the created [PropertyBuilder] instance + */ + fun toBuilder(): PropertyBuilder { + val builder = PropertyBuilder(this.name, this.type) + builder.modifiers.addAll(this.modifiers) + builder.annotations.addAll(this.annotations) + builder.initBlock = this.initBlock + builder.docs.addAll(this.docs) + builder.modifiers.addAll(this.modifiers) + return builder + } + + /** + * The companion object contains some helper methods to create a new instance from the [PropertyBuilder]. + */ + companion object { + + /** + * Create a [PropertyBuilder] with the specified property name, [ClassName] type, and optional modifiers. + * + * @param name the name of the property + * @param type the [ClassName] representing the type of the property + * @param modifiers an array of modifiers to apply to the property. Defaults to an empty array if not provided + * @return a [PropertyBuilder] instance configured with the specified parameters + */ + @JvmStatic + fun builder( + name: String, + type: ClassName, + vararg modifiers: DartModifier = emptyArray() + ): PropertyBuilder { + return PropertyBuilder(name, type).modifiers(*modifiers) + } + + /** + * Create a [PropertyBuilder] with the specified property name, [TypeName] type, and optional modifiers. + * + * @param name the name of the property + * @param type the [TypeName] representing the type of the property + * @param modifiers an array of modifiers to apply to the property. Defaults to an empty array if not provided + * @return a [PropertyBuilder] instance configured with the specified parameters + */ + @JvmStatic + fun builder( + name: String, + type: TypeName, + vararg modifiers: DartModifier = emptyArray() + ): PropertyBuilder { + return PropertyBuilder(name, type).modifiers(*modifiers) + } + + /** + * Create a [PropertyBuilder] with the specified property name, [KClass] type, and optional modifiers. + * + * @param name the name of the property + * @param type the [KClass] representing the type of the property + * @param modifiers an array of modifiers to apply to the property. Defaults to an empty array if not provided + * @return a [PropertyBuilder] instance configured with the specified parameters + */ + @JvmStatic + fun builder( + name: String, + type: KClass<*>, + vararg modifiers: DartModifier = emptyArray() + ): PropertyBuilder { + return PropertyBuilder(name, type.asTypeName()).modifiers(*modifiers) + } + + /** + * Create a [PropertyBuilder] with only the property name. + * + * @param name the name of the property + * @return a [PropertyBuilder] instance configured with the specified parameters + */ + @JvmStatic + fun constBuilder(name: String): PropertyBuilder = + PropertyBuilder(name).modifiers(*ALLOWED_CONST_MODIFIERS.toTypedArray()) + + /** + * Create a [PropertyBuilder] with only the property name and a given type. + * + * @param name the name of the property + * @param type the [TypeName] representing the type of the property + * @return a [PropertyBuilder] instance + */ + @JvmStatic + fun constBuilder(name: String, type: TypeName): PropertyBuilder = + PropertyBuilder(name, type).modifiers(*ALLOWED_CONST_MODIFIERS.toTypedArray()) + + /** + * Create a [PropertyBuilder] with only the property name and a given type. + * + * @param name the name of the property + * @param type the [KClass] representing the type of the property + * @return a [PropertyBuilder] instance + */ + @JvmStatic + fun constBuilder(name: String, type: KClass<*>): PropertyBuilder = + PropertyBuilder(name, type.asTypeName()).modifiers(*ALLOWED_CONST_MODIFIERS.toTypedArray()) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/property/consts/ConstantPropertyBuilder.kt b/src/main/kotlin/net/theevilreaper/dartpoet/property/consts/ConstantPropertyBuilder.kt new file mode 100644 index 00000000..7c619ad9 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/property/consts/ConstantPropertyBuilder.kt @@ -0,0 +1,56 @@ +package net.theevilreaper.dartpoet.property.consts + +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.code.CodeBlock +import net.theevilreaper.dartpoet.property.PropertySpec +import net.theevilreaper.dartpoet.type.TypeName + +/** + * The [ConstantPropertyBuilder] can be used to construct new [ConstantPropertySpec] object reference which can be set + * into a [ConstantPropertySpec]. + * @author theEvilReaper + * @since 1.0.0 + */ +class ConstantPropertyBuilder internal constructor( + val name: String, + val typeName: TypeName? = null, + val modifiers: Set +) { + internal var initializer: CodeBlock.Builder = CodeBlock.Builder() + internal var isPrivate: Boolean = false + + /** + * Apply a given format which contains the parts for the init block of the [PropertySpec]. + * @param format the given format + * @param args the arguments for the format + */ + fun initWith(format: String, vararg args: Any?) = apply { + this.initializer.add(format, *args) + } + + /** + * Set the initializer block directly as [CodeBlock.Builder] to the property. + * @param codeFragment the [CodeBlock.Builder] to set + */ + fun initWith(codeFragment: CodeBlock.Builder) = apply { + this.initializer = codeFragment + } + + /** + * Indicates if the property should be private. + * The option is only allowed when to property is no file level constant property. + * @param boolean True for a private property otherwise false + * @return the current [ConstantPropertyBuilder] instance + */ + fun private(boolean: Boolean) = apply { + this.isPrivate = boolean + } + + /** + * Constructs a [ConstantPropertySpec] reference from the current builder instance + * @return the created [ConstantPropertySpec] instance + */ + fun build(): ConstantPropertySpec { + return ConstantPropertySpec(this) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/property/consts/ConstantPropertySpec.kt b/src/main/kotlin/net/theevilreaper/dartpoet/property/consts/ConstantPropertySpec.kt new file mode 100644 index 00000000..65d8ed7c --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/property/consts/ConstantPropertySpec.kt @@ -0,0 +1,174 @@ +package net.theevilreaper.dartpoet.property.consts + +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.buildCodeString +import net.theevilreaper.dartpoet.code.writer.ConstantPropertyWriter +import net.theevilreaper.dartpoet.property.PropertySpec +import net.theevilreaper.dartpoet.type.ClassName +import net.theevilreaper.dartpoet.type.TypeName +import net.theevilreaper.dartpoet.type.asTypeName +import net.theevilreaper.dartpoet.util.ALLOWED_CLASS_CONST_MODIFIERS +import net.theevilreaper.dartpoet.util.ALLOWED_CONST_MODIFIERS +import net.theevilreaper.dartpoet.util.toImmutableSet +import kotlin.reflect.KClass + +/** + * The [ConstantPropertySpec] is special implementation which contains the same structure as the [PropertySpec] implementation. + * It's separated to avoid any conflicts with the [PropertySpec] implementation. Tge separation also reduces the complexity + * of the writer which is responsible for the generation of the code for properties. + * A file constant can't have the ability to have more modifiers then [DartModifier.CONST]. + * @author theEvilReaper + * @since 1.0.0 + */ +class ConstantPropertySpec( + builder: ConstantPropertyBuilder +) { + internal val name = builder.name + internal val typeName = builder.typeName + internal val initializer = builder.initializer + internal val isPrivate = builder.isPrivate + internal val modifiers = builder.modifiers.toImmutableSet() + + init { + require(name.trim().isNotEmpty()) { "The name of a file constant can't be empty" } + require(initializer.isNotEmpty()) { "The initializer can't be empty" } + + if (this.modifiers.size == 1 && this.modifiers.first() == DartModifier.CONST && isPrivate) { + throw IllegalArgumentException("A file constant can't be private") + } + } + + /** + * Trigger the write process from the [ConstantPropertyWriter] to write the spec into dart code. + * @param codeWriter the [CodeWriter] to apply the content from the spec + */ + internal fun write(codeWriter: CodeWriter) { + ConstantPropertyWriter().write(this, codeWriter) + } + + /** + * Returns a textual representation of the spec class. + * It calls the [write] method to get the representation + * @return the created representation + */ + override fun toString() = buildCodeString { write(this) } + + /** + * Creates a new instance builder instance with the values from a given [ConstantPropertySpec] reference. + * @return the created instance from the [ConstantPropertySpec] + */ + fun toBuilder(): ConstantPropertyBuilder { + val builder = ConstantPropertyBuilder(name, typeName, modifiers) + builder.initializer = initializer + builder.isPrivate = isPrivate + return builder + } + + /** + * The companion object contains some helper methods to create a new instance from the [ConstantPropertyBuilder]. + */ + companion object { + + /** + * Create a new builder reference to create [ConstantPropertySpec]. + * This method should be used for the constants for a class. + * Adding a [classConst] to a file occurs an error. + * + * @param name the name for the property + * @param type the type for the property provided as [ClassName] + * @param type The type of the constant property. + * @return an instance of [ConstantPropertyBuilder] representing the constant property + */ + @JvmStatic + fun classConst( + name: String, + type: ClassName + ) = ConstantPropertyBuilder(name, type, ALLOWED_CONST_MODIFIERS) + + /** + * Create a new builder reference to create [ConstantPropertySpec]. + * This method should be used for the constants for a class. + * Adding a [classConst] to a file occurs an error. + * + * @param name the name for the property + * @param type the type for the property provided as [TypeName] + * @return an instance of [ConstantPropertyBuilder] representing the constant property + */ + @JvmStatic + fun classConst( + name: String, + type: TypeName + ) = ConstantPropertyBuilder(name, type, ALLOWED_CONST_MODIFIERS) + + /** + * Create a new builder reference to create [ConstantPropertySpec]. + * This method should be used for the constants for a class. + * Adding a [classConst] to a file occurs an error. + * + * @param name the name for the property + * @param type the type for the property provided as [KClass] + * @return an instance of [ConstantPropertyBuilder] representing the constant property. + */ + @JvmStatic + fun classConst( + name: String, + type: KClass<*> + ) = ConstantPropertyBuilder(name, type.asTypeName(), ALLOWED_CONST_MODIFIERS) + + /** + * Create a new builder reference to create [ConstantPropertySpec]. + * This method should be used for the constants for a class. + * Adding a [classConst] to a file occurs an error. + * + * @param name the name for the property + * @return an instance of [ConstantPropertyBuilder] representing the constant property + */ + @JvmStatic + fun classConst( + name: String, + ) = ConstantPropertyBuilder(name, null, ALLOWED_CONST_MODIFIERS) + + /** + * Creates a new instance from the [ConstantPropertyBuilder]. + * @param name the name of the property as [ClassName] + * @return the created instance from the [ConstantPropertyBuilder] + */ + @JvmStatic + fun fileConst( + name: String, + type: ClassName, + ) = ConstantPropertyBuilder(name, type, ALLOWED_CLASS_CONST_MODIFIERS) + + /** + * Creates a new instance from the [ConstantPropertyBuilder]. + * @param name the name of the property + * @param type the type for the property as [TypeName] + * @return the created instance from the [ConstantPropertyBuilder] + */ + @JvmStatic + fun fileConst( + name: String, + type: TypeName, + ) = ConstantPropertyBuilder(name, type, ALLOWED_CLASS_CONST_MODIFIERS) + + /** + * Creates a new instance from the [ConstantPropertyBuilder]. + * @param name the name of the property + * @param type the type for the property as [KClass] + * @return the created instance from the [ConstantPropertyBuilder] + */ + fun fileConst( + name: String, + type: KClass<*>, + ) = ConstantPropertyBuilder(name, type.asTypeName(), ALLOWED_CLASS_CONST_MODIFIERS) + + /** + * Creates a new instance from the [ConstantPropertyBuilder]. + * @param name the name of the property + * @return the created instance from the [ConstantPropertyBuilder] + */ + @JvmStatic + fun fileConst(name: String) = ConstantPropertyBuilder(name, null, ALLOWED_CLASS_CONST_MODIFIERS) + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/type/ClassName.kt b/src/main/kotlin/net/theevilreaper/dartpoet/type/ClassName.kt new file mode 100644 index 00000000..820cb9e5 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/type/ClassName.kt @@ -0,0 +1,80 @@ +package net.theevilreaper.dartpoet.type + +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.util.ALLOWED_PRIMITIVE_TYPES +import net.theevilreaper.dartpoet.util.NULLABLE_CHAR +import kotlin.reflect.KClass + +/** + * A class representing a custom type named [ClassName]. + * It can be used to model class names, interface names, typedef names, and enum names which are not built-in types. + * + * @param name the name of the [ClassName] + * @param isNullable a flag indicating whether the [ClassName] can be null (default is false). + */ +open class ClassName( + val name: String, + isNullable: Boolean = false +) : TypeName(isNullable) { + + init { + require(name.trim().isNotEmpty()) { "The name of a ClassName can't be empty (includes only spaces)" } + } + + /** + * Emits the name of the [ClassName] to a [CodeWriter]. + * + * @param out the [CodeWriter] instance to which the name is emitted + * @return the same [CodeWriter] instance for method chaining + */ + override fun emit(out: CodeWriter): CodeWriter { + out.emit(name) + + if (isNullable) { + out.emit(NULLABLE_CHAR) + } + return out + } + + /** + * Creates a copy of the [ClassName] with an optional nullable flag. + * + * @param nullable a flag indicating whether the copied [ClassName] can be null + * @return a new [ClassName] instance with the provided nullable flag + */ + override fun copy(nullable: Boolean): TypeName { + return ClassName(name, nullable) + } + + override fun getRawData(): String = name +} + +/** + * Converts a generic [KClass] instance to a [ClassName] instance. + * @return the created [ClassName] instance + */ +@JvmName("get") +fun KClass<*>.asClassName(): ClassName { + val simpleName = this.simpleName!! + if (this.simpleName in ALLOWED_PRIMITIVE_TYPES) { + return TypeName.parseSimpleKClass(this) + } + + if (this == Void::class) { + return ClassName(Void::class.simpleName!!.replaceFirstChar { it.lowercase() }) + } + return ClassName(simpleName) +} + +/** + * Converts a generic [Class] instance to a [ClassName] instance. + * @return the created [ClassName] instance + * @throws IllegalArgumentException if the class is a primitive type, a void type or an array + */ +@JvmName("get") +fun Class<*>.asClassName(): ClassName { + require(!isPrimitive) { "A primitive type can't be represented over a ClassName!" } + require(Void.TYPE != this) { "A void type can't be represented over a ClassName!" } + require(!isArray) { "An array can't be represented over a ClassName!" } + return ClassName(this.simpleName) +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/type/DynamicClassName.kt b/src/main/kotlin/net/theevilreaper/dartpoet/type/DynamicClassName.kt new file mode 100644 index 00000000..30c5bd78 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/type/DynamicClassName.kt @@ -0,0 +1,26 @@ +package net.theevilreaper.dartpoet.type + +import net.theevilreaper.dartpoet.DartModifier +import org.jetbrains.annotations.ApiStatus + +/** + * [DynamicClassName] extends [ClassName] and is used to represent the 'dynamic' type in Dart programming language. + * It provides a mechanism to handle the 'dynamic' type, which is not typically used for copying operations. + * + * @constructor Creates an instance of [DynamicClassName] + * @author theEvilReaper + * @since 1.0.0 + */ +@ApiStatus.Internal +internal class DynamicClassName : ClassName(DartModifier.DYNAMIC.identifier) { + + /** + * This method is overridden from the superclass and throws an [IllegalAccessException] with a message + * indicating that the 'dynamic' type cannot be copied. + * + * @param nullable indicates whether the type is nullable + * @throws IllegalAccessException always throws an exception indicating that the 'dynamic' type cannot be copied + */ + @Throws(IllegalAccessException::class) + override fun copy(nullable: Boolean) = throw IllegalAccessException("The dynamic type can't be copied") +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/type/ParameterizedTypeName.kt b/src/main/kotlin/net/theevilreaper/dartpoet/type/ParameterizedTypeName.kt new file mode 100644 index 00000000..b4fd850c --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/type/ParameterizedTypeName.kt @@ -0,0 +1,165 @@ +package net.theevilreaper.dartpoet.type + +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.util.EMPTY_STRING +import org.jetbrains.annotations.ApiStatus +import java.lang.reflect.Type +import kotlin.reflect.KClass + +/** + * Represents a parameterized type name, which includes information about the + * raw type, its type arguments, and whether it is nullable. + * + * @param enclosingTypeName (Optional) The enclosing type name if this parameterized type is nested within another type + * @param rawType The raw type of this parameterized type + * @param typeArguments The list of type arguments associated with the raw type + * @param nullable Specifies whether this parameterized type is nullable (default is false) + * + * @author theEvilReaper + * @since 1.0.0 + */ +@ApiStatus.Internal +class ParameterizedTypeName internal constructor( + private val enclosingTypeName: TypeName?, + private val rawType: ClassName, + private val typeArguments: List, + nullable: Boolean = false +) : TypeName(nullable) { + + /** + * Performs some check to the given values from the constructor. + */ + init { + require(typeArguments.isNotEmpty() || enclosingTypeName != null) { + "no type arguments: $rawType" + } + } + + /** + * Creates a copy of the [ParameterizedTypeName] with an optional nullable flag. + * + * @param nullable a flag indicating whether the copied [ParameterizedTypeName] can be null + * @return a new [ParameterizedTypeName] instance with the provided nullable flag + */ + override fun copy(nullable: Boolean): ParameterizedTypeName { + return ParameterizedTypeName(enclosingTypeName, rawType, typeArguments, nullable) + } + + /** + * Creates a copy of the [ParameterizedTypeName] with an optional nullable flag. + * + * @param nullable a flag indicating whether the copied [ParameterizedTypeName] can be null + * @param typeArguments (Optional) the list of type arguments for the new [ParameterizedTypeName]. + * If not provided, the type arguments from the current instance will be used + * + * @return a new [ParameterizedTypeName] instance with the provided nullable flag + */ + fun copy(nullable: Boolean, typeArguments: List = this.typeArguments): ParameterizedTypeName { + return ParameterizedTypeName(enclosingTypeName, rawType, typeArguments, nullable) + } + + /** + * Emits the structure of the [ParameterizedTypeName] to a [CodeWriter]. + * + * @param out the [CodeWriter] instance to which the name is emitted + * @return the same [CodeWriter] instance for method chaining + */ + override fun emit(out: CodeWriter): CodeWriter { + if (enclosingTypeName != null) { + enclosingTypeName.emit(out) + out.emit("." + rawType.name) + } else { + rawType.emit(out) + } + + if (typeArguments.isNotEmpty()) { + out.emit("<") + typeArguments.forEachIndexed { index, typeName -> + if (index > 0) out.emit(", ") + typeName.emit(out) + } + out.emit(">") + } + return out + } + + /** + * Returns the raw data from a [ParameterizedTypeName] instance. + * It contains the type arguments joined as a [String]. + * This method is only used internally in the project to check if the generic type is the same as the enclosing type. + * @return the raw data from a [ParameterizedTypeName] instance as [String] + */ + override fun getRawData(): String { + if (typeArguments.isEmpty()) return EMPTY_STRING + return typeArguments.joinToString(separator = ", ") + } + + /** + * Returns a copy of this [ParameterizedTypeName] with the provided type arguments. + */ + companion object { + + /** + * Creates a parameterized type using a [ClassName] as the raw type and the provided type arguments. + * + * @param typeArguments The type arguments to parameterize this class with + * @return a [ParameterizedTypeName] representing the parameterized type + */ + @JvmStatic + @JvmName("get") + fun ClassName.parameterizedBy( + vararg typeArguments: TypeName, + ): ParameterizedTypeName = ParameterizedTypeName(null, this, typeArguments.toList()) + + /** + * Creates a parameterized type using a [ClassName] as the raw type and the provided type arguments. + * + * @param typeArguments The type arguments to parameterize this class with, provided as [KClass] instances + * @return a [ParameterizedTypeName] representing the parameterized type + */ + @JvmStatic + @JvmName("get") + fun ClassName.parameterizedBy( + vararg typeArguments: KClass<*>, + ): ParameterizedTypeName = ParameterizedTypeName(null, this, typeArguments.map { it.asTypeName() }) + + /** + * Creates a parameterized type using a generic [Class] as the raw type and the provided type arguments. + * + * @param typeArguments The type arguments to parameterize this class with, provided as [Type] instances + * @return a [ParameterizedTypeName] representing the parameterized type + */ + @JvmStatic + @JvmName("get") + fun Class<*>.parameterizedBy( + vararg typeArguments: Type, + ): ParameterizedTypeName = + ParameterizedTypeName(null, asClassName(), typeArguments.map { it.asTypeName() }) + + /** + * Creates a parameterized type using a [KClass] as the raw type and the provided type arguments. + * + * @param typeArguments The type arguments to parameterize this class with, provided as [KClass] instances + * @return a [ParameterizedTypeName] representing the parameterized type + */ + @JvmStatic + @JvmName("get") + fun KClass<*>.parameterizedBy( + vararg typeArguments: KClass<*> + ): ParameterizedTypeName = + ParameterizedTypeName(null, asClassName(), typeArguments.map { it.asTypeName() }) + + /** + * Creates a parameterized type using a [KClass] as the raw type and the provided type arguments. + * + * @param typeArguments The type arguments to parameterize this class with, provided as [TypeName] instances + * @return a [ParameterizedTypeName] representing the parameterized type + */ + @JvmStatic + @JvmName("get") + fun KClass<*>.parameterizedBy( + vararg typeArguments: TypeName + ): ParameterizedTypeName = + ParameterizedTypeName(null, asClassName(), typeArguments.map { it }) + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/type/TypeName.kt b/src/main/kotlin/net/theevilreaper/dartpoet/type/TypeName.kt new file mode 100644 index 00000000..ea9791b2 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/type/TypeName.kt @@ -0,0 +1,169 @@ +package net.theevilreaper.dartpoet.type + +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.buildCodeString +import net.theevilreaper.dartpoet.util.NULLABLE_CHAR +import java.lang.reflect.Type +import kotlin.reflect.KClass + +/** + * Represents a sealed class hierarchy for custom type names. + * + * The [TypeName] class hierarchy is designed to handle custom type names, including their nullability information. + * It serves as a base class for various custom type implementations and provides the framework for emitting string + * representations, creating copies with different nullability, and comparing instances based on their cached string + * representations. + * + * @param isNullable a flag indicating whether the custom type can be null (default is false) + * @author theEvilReaper + * @since 1.0.0 + */ +sealed class TypeName(val isNullable: Boolean) { + + /** + * Lazily computes and caches a string representation of this [TypeName] instance. + * + * This method generates a string representation of the [TypeName], taking into account whether it is nullable or not, + * and caches the result. Subsequent calls to [toString] return the cached value, avoiding unnecessary computation. + * + * @return the string representation of this [TypeName] instance + */ + private val cachedString: String by lazy { + buildCodeString { + emit(this) + if (isNullable) emit(NULLABLE_CHAR) + } + } + + /** + * Returns the cached string representation of this [TypeName] instance. + * + * This method retrieves the cached string representation of the [TypeName] generated by [cachedString]. + * It ensures efficient reuse of the previously computed string value. + * + * @return the cached string representation of this [TypeName] instance + */ + override fun toString(): String = cachedString + + /** + * Emits a string representation of this [TypeName] to the specified [CodeWriter]. + * + * This method is responsible for emitting the string representation of the [TypeName] to a [CodeWriter] instance. + * Subclasses must override this method to provide their specific implementation for generating the string + * representation. + * + * @param out the [CodeWriter] instance to which the string representation is emitted + * @return the same [CodeWriter] instance for method chaining + */ + internal abstract fun emit(out: CodeWriter): CodeWriter + + /** + * Returns the raw data from a [TypeName] instance. + * This method is only used internally in the project to check if the generic type is the same as the enclosing type. + * @return the raw data from a [TypeName] instance as [String] + */ + internal abstract fun getRawData(): String + + /** + * Creates a copy of this [TypeName] with an optional nullable flag. + * + * This method generates a new instance of a [TypeName] based on the current instance, optionally allowing the + * nullable flag to be updated. Subclasses should implement this method to return a new instance with the provided + * nullable flag. + * + * @param nullable a flag indicating whether the copied [TypeName] should be nullable (default is the current value) + * @return a new [TypeName] instance with the specified nullable flag + */ + internal abstract fun copy(nullable: Boolean = this.isNullable): TypeName + + /** + * Checks if this [TypeName] is equal to another object. + * + * This method compares this [TypeName] instance with another object and returns `true` if they are equal. + * Equality is determined by comparing the cached string representations of the two [TypeName] instances. + * + * @param other the object to compare with this [TypeName] + * @return `true` if this [TypeName] is equal to the provided object, `false` otherwise + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as TypeName + + return cachedString == other.cachedString + } + + /** + * Computes the hash code of this [TypeName]. + * + * This method generates a hash code for this [TypeName] based on its cached string representation + * + * @return the hash code of this [TypeName] + */ + override fun hashCode(): Int = cachedString.hashCode() + + /** + * The companion object contains some helper methods to create a new instance from the [TypeName]. + */ + companion object { + + /** + * Converts a [Type] from the jdk into a corresponding Dart type represented by a [TypeName]. + * + * This method is used to map Java types to Dart types. It handles primitive types, arrays, and other custom types. + * + * @param type the Java type to be converted + * @return the Dart type represented as a [TypeName] + * @throws IllegalArgumentException if the provided [Type] is not a supported type or if an array type is encountered + * (arrays are not supported at the moment) + */ + @Throws(IllegalArgumentException::class) + internal fun get(type: Type): TypeName { + return when (type) { + is Class<*> -> when { + type === Boolean::class.javaPrimitiveType -> BOOLEAN + type === Int::class.javaPrimitiveType -> INTEGER + type === Long::class.javaPrimitiveType -> INTEGER + type === Double::class.javaPrimitiveType -> DOUBLE + type === Float::class.javaPrimitiveType -> DOUBLE + type === String::class.javaPrimitiveType -> STRING + type.isArray -> throw IllegalArgumentException("An array type is not supported at the moment") + else -> type.asClassName() + } + + else -> throw IllegalArgumentException("Received unexpected type $type") + } + } + + /** + * Parses a [KClass] representing a primitive data type in Dart to a matching [ClassName] constant. + * + * This method is used to map Kotlin primitive types to corresponding [ClassName] constants, which are used + * to represent Dart primitive data types. + * + * @param type the [KClass] to be mapped to a [ClassName] + * @return the mapped [ClassName] constant + * @throws IllegalArgumentException if the given [KClass] is not a supported primitive class + */ + @Throws(IllegalArgumentException::class) + internal fun parseSimpleKClass(type: KClass<*>): ClassName { + return when (type) { + Boolean::class -> BOOLEAN + Int::class -> INTEGER + Long::class -> INTEGER + Double::class -> DOUBLE + Float::class -> DOUBLE + String::class -> STRING + else -> throw IllegalArgumentException("The given $type is not a primitive object") + } + } + } +} + +@JvmName("get") +fun KClass<*>.asTypeName(): TypeName = asClassName() + +/** Returns a [TypeName] equivalent to this [Type]. */ +@JvmName("get") +fun Type.asTypeName(): TypeName = TypeName.get(this) diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/type/TypeNames.kt b/src/main/kotlin/net/theevilreaper/dartpoet/type/TypeNames.kt new file mode 100644 index 00000000..edf038d9 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/type/TypeNames.kt @@ -0,0 +1,41 @@ +package net.theevilreaper.dartpoet.type + +import net.theevilreaper.dartpoet.annotation.AnnotationSpec + +/** + * The file contains some common used [ClassName] instances which are used in the library. + * It contains only the primitive types which are supported by Dart. + * @since 1.0.0 + */ + +// Represents the boolean type in Dart +@JvmField +val BOOLEAN: ClassName = ClassName("bool") + +// Represents the integer type in Dart +@JvmField +val INTEGER: ClassName = ClassName("int") + +// Represents the double type in Dart +@JvmField +val DOUBLE: ClassName = ClassName("double") + +// Represents the string type in Dart +@JvmField +val STRING: ClassName = ClassName("String") + +// Represents the dynamic type in Dart +@JvmField +val DYNAMIC: ClassName = DynamicClassName() + +// Represents the pragma metadata annotation from Dart +@JvmField +val PRAGMA: AnnotationSpec = AnnotationSpec.builder("pragma").build() + +// Represents the override metadata annotation from Dart +@JvmField +val OVERRIDE: AnnotationSpec = AnnotationSpec.builder("override").build() + +// Represents the deprecated metadata annotation from Dart +@JvmField +val DEPRECATED: AnnotationSpec = AnnotationSpec.builder("deprecated").build() diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/util/Constants.kt b/src/main/kotlin/net/theevilreaper/dartpoet/util/Constants.kt index 4df13bd0..9622203a 100644 --- a/src/main/kotlin/net/theevilreaper/dartpoet/util/Constants.kt +++ b/src/main/kotlin/net/theevilreaper/dartpoet/util/Constants.kt @@ -1,14 +1,103 @@ package net.theevilreaper.dartpoet.util -import java.util.regex.Pattern +import net.theevilreaper.dartpoet.DartModifier -internal const val DEFAULT_INDENT = " " +// The documentation from dart says that maximum length of a line is 80 +internal const val MAX_LINE_LENGTH = 80 internal const val EMPTY_STRING = "" internal const val NULL_STRING = "null" -internal const val NEW_LINE = "\n" -internal const val SEMICONLON = ";" +internal const val NULLABLE_CHAR = "?" -internal const val AS_PART = "as" +internal const val SPACE_CHAR = ' ' +internal const val SPACE = SPACE_CHAR.toString() +const val DEFAULT_INDENT = " " + +internal const val NEW_LINE_CHAR = '\n' +internal const val NEW_LINE = NEW_LINE_CHAR.toString() +internal const val SEMICOLON = ";" +internal const val DOCUMENTATION_CHAR = "///" internal const val IMPORT = "import" +internal const val ANNOTATION_CHAR = "@" +internal const val DART_FILE_ENDING = ".dart" + +//Brackets +internal const val CURLY_OPEN = '{' +internal const val CURLY_CLOSE = '}' +internal const val ROUND_OPEN = "(" +internal const val ROUND_CLOSE = ")" + +internal val ALLOWED_FUNCTION_MODIFIERS = setOf(DartModifier.PUBLIC, DartModifier.PRIVATE, DartModifier.STATIC, DartModifier.ABSTRACT) +internal val ALLOWED_PROPERTY_MODIFIERS = + setOf(DartModifier.PRIVATE, DartModifier.FINAL, DartModifier.LATE, DartModifier.STATIC, DartModifier.CONST) +internal val ALLOWED_CLASS_CONST_MODIFIERS = setOf(DartModifier.CONST) +internal val ALLOWED_CONST_MODIFIERS = setOf(DartModifier.STATIC, DartModifier.CONST) + +//RegEx +private val namePattern: Regex = Regex("^[a-z]+(?:_[a-z]+)*\$") +private val lowerCamelCase: Regex = Regex("[a-z]+[A-Z0-9]*[a-z0-9]*[A-Za-z0-9]*") +private val indentPattern: Regex = Regex(" +") + +internal val ALLOWED_PRIMITIVE_TYPES = setOf("Short", "Int", "Long", "Float", "Double", "Char", "Boolean") + +//Error message +internal const val NO_PARAMETER_TYPE = "Parameter must have a type" + +/** + * Checks if a given set of [DartModifier] matches with a given set which contains the allowed [DartModifier]. + * @param rawModifiers contains all modifiers from the context + * @param allowedModifiers contains all modifiers which are allowed for context + * @param context contains the context from where the method is called + */ +fun hasAllowedModifiers(rawModifiers: Set, allowedModifiers: Set, context: String) { + rawModifiers.also { + LinkedHashSet(it).apply { + removeAll(allowedModifiers) + require(isEmpty()) { "These modifiers $this are not allowed in a $context context. Allowed modifiers: $allowedModifiers" } + } + } +} + +/** + * Checks if a filename matches the DartFile conventions for file names (not class names!) + * @param fileName the file name to check + * @return true if the name matches otherwise false + */ +fun isDartConventionFileName(fileName: String): Boolean { + if (fileName.trim().isEmpty()) return false + if (fileName.contains(DART_FILE_ENDING)) { + return fileName.replace(DART_FILE_ENDING, EMPTY_STRING).matches(namePattern) + } + return fileName.matches(namePattern) +} + +/** + * Return a [Boolean] if the given string has the lowerCamelCase format. + * @param input the string to check + * @return true when the string has the format otherwise false + */ +fun isInLowerCamelCase(input: String): Boolean { + return testStringForPattern(input, lowerCamelCase) +} + +/** + * Checks if a given [String] can be used as an indent. + * @param input the string to check + * @return true when the string only contains spaces otherwise false + */ +fun isIndent(input: String): Boolean { + return testStringForPattern(input, indentPattern) +} + +/** + * Add base method for all the method which tests a string for a pattern. + * @param input the string to check + * @param pattern the pattern for the check + * @return true when the string matches with the pattern otherwise false + */ +private fun testStringForPattern(input: String, pattern: Regex): Boolean { + return input.isNotEmpty() && input.matches(pattern) +} -internal val NEW_LINE_SPLIT: Pattern = Pattern.compile(NEW_LINE) +fun formatLowerCamelCase(input: String): String { + return input.replaceFirstChar { it.lowercase() } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/util/DirectiveOrdere.kt b/src/main/kotlin/net/theevilreaper/dartpoet/util/DirectiveOrdere.kt new file mode 100644 index 00000000..23d3ab90 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/util/DirectiveOrdere.kt @@ -0,0 +1,78 @@ +package net.theevilreaper.dartpoet.util + +import net.theevilreaper.dartpoet.directive.BaseDirective +import net.theevilreaper.dartpoet.directive.Directive +import org.jetbrains.annotations.ApiStatus +import kotlin.reflect.KClass + +/** + * Object responsible for sorting and ordering directives. + * + * This utility object provides functions to sort lists of directives either in alphabetical order based on + * their raw paths or based on a specified subtype of [Directive]. It is designed to work with instances of + * classes that inherit from [Directive]. + * + * @since 1.0.0 + * @version 1.0.0 + * @author theEvilReaper + */ +@ApiStatus.Internal +internal object DirectiveOrdering { + + /** + * Sorts a list of directives in alphabetical order based on their raw paths. + * + * This function takes a list of directives, where each directive is a subtype of [BaseDirective], + * and sorts them in ascending alphabetical order according to their raw paths. + * + * @param directives the list of directives to be sorted + * @return a new list containing the sorted directives + */ + internal inline fun sortDirectives(directives: List): List { + if (directives.isEmpty()) return emptyList() + return directives.sortedBy { it.getRawPath() }.toImmutableList() + } + + /** + * Sorts a list of directives based on a specified subtype of [Directive]. + * + * This function filters the given list of directives to include only instances of the specified + * subtype [T], compares them based on their raw paths, and returns a new list sorted in ascending order. + * + * @param directiveInstance the class type representing the specific subtype [T] to filter the directives + * @param directives the list of directives to be sorted + * @return a new list containing the sorted directives of the specified subtype [T] + */ + internal inline fun sortDirectives( + directiveInstance: KClass, + directives: List + ): List { + if (directives.isEmpty()) return emptyList() + return directives.filter { it::class == directiveInstance }.sortedBy { it.getRawPath() }.toImmutableList() + } + + /** + * Sorts a list of directives based on a specified subtype of [Directive] and a predicate. + * + * This function filters the given list of directives to include only instances of the specified + * subtype [T], compares them based on their raw paths, and returns a new list sorted in ascending order. + * The predicate is used to filter the directives based on their raw paths. + * + * @param directiveInstance the class type representing the specific subtype [T] to filter the directives + * @param directives the list of directives to be sorted + * @param predicate the predicate used to filter the directives + * @return a new list containing the sorted directives of the specified subtype [T] + */ + internal inline fun sortDirectives( + directiveInstance: KClass, + directives: List, + crossinline predicate: (String) -> Boolean + ): List { + if (directives.isEmpty()) return emptyList() + return directives + .filter { it::class == directiveInstance } + .sortedBy { it.getRawPath() } + .filter { predicate(it.asString()) } + .toImmutableList() + } +} diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/util/ParameterFilter.kt b/src/main/kotlin/net/theevilreaper/dartpoet/util/ParameterFilter.kt new file mode 100644 index 00000000..bcef4c11 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/util/ParameterFilter.kt @@ -0,0 +1,25 @@ +package net.theevilreaper.dartpoet.util + +import net.theevilreaper.dartpoet.parameter.ParameterSpec +import org.jetbrains.annotations.ApiStatus + +/** + * Utility class for filtering [ParameterSpec] lists. + * @since 1.0.0 + * @version 1.0.0 + * @author theEvilReaper + */ +@ApiStatus.Internal +internal object ParameterFilter { + + /** + * Filters the given [ParameterSpec] list by the given predicate. + * @param parameters the list of [ParameterSpec] to filter + * @param predicate the predicate to filter the list + * @return the filtered list + */ + internal inline fun filterParameter(parameters: List, crossinline predicate: (ParameterSpec) -> Boolean): List { + if (parameters.isEmpty()) return emptyList() + return parameters.filter(predicate).toImmutableList() + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/util/StringHelper.kt b/src/main/kotlin/net/theevilreaper/dartpoet/util/StringHelper.kt new file mode 100644 index 00000000..a1203892 --- /dev/null +++ b/src/main/kotlin/net/theevilreaper/dartpoet/util/StringHelper.kt @@ -0,0 +1,43 @@ +package net.theevilreaper.dartpoet.util + +import net.theevilreaper.dartpoet.DartModifier + +/** + * Utility class for string operations which are used to generate code parts. + * @since 1.0.0 + * @version 1.0.0 + * @author theEvilReaper + */ +internal object StringHelper { + + /** + * Joins a given [Set] of [DartModifier] to a string representation. + * @param modifiers the [Set] of [DartModifier] to join + * @param prefix the prefix for the string + * @param separator the separator for the string + * @param postfix the postfix for the string + * @param value the function to get the value from the [DartModifier] + * @return the joined string or an empty string if the [Set] is empty + */ + inline fun joinModifiers( + modifiers: Set, + prefix: String = EMPTY_STRING, + separator: String = EMPTY_STRING, + postfix: String = EMPTY_STRING, + crossinline value: (DartModifier) -> String = { it.identifier } + ): String { + if (modifiers.isEmpty()) return EMPTY_STRING + return modifiers.joinToString(prefix = prefix, separator = separator, postfix = postfix) { value(it) } + } + + /** + * Ensures that the given variable name has the private modifier if the [withPrivate] parameter is true. + * @param name the name of the variable + * @param withPrivate the flag to check if the variable name should have the private modifier + * @return the variable name with the private modifier or the name itself + */ + fun ensureVariableNameWithPrivateModifier(name: String, withPrivate: Boolean): String { + require(name.trim().isNotEmpty()) { "The name parameter can't be empty" } + return if (!withPrivate) name else "${DartModifier.PRIVATE.identifier}$name" + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/util/Util.kt b/src/main/kotlin/net/theevilreaper/dartpoet/util/Util.kt index d57c57fc..3267bdd5 100644 --- a/src/main/kotlin/net/theevilreaper/dartpoet/util/Util.kt +++ b/src/main/kotlin/net/theevilreaper/dartpoet/util/Util.kt @@ -1,11 +1,167 @@ +/* + * Copyright (C) 2015 Square, 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 + * + * https://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. + * + * Changes to this class: + * - it contains only the methods which are needed + */ package net.theevilreaper.dartpoet.util -import java.util.* +import java.util.Collections import kotlin.collections.ArrayList import kotlin.collections.LinkedHashSet +internal fun Collection.toImmutableList(): List = + Collections.unmodifiableList(ArrayList(this)) + internal fun Collection.toImmutableSet(): Set = Collections.unmodifiableSet(LinkedHashSet(this)) -internal fun Collection.toImmutableList(): List = - Collections.unmodifiableList(ArrayList(this)) +internal fun T.isOneOf(t1: T, t2: T, t3: T? = null, t4: T? = null, t5: T? = null, t6: T? = null) = + this == t1 || this == t2 || this == t3 || this == t4 || this == t5 || this == t6 + +internal fun Collection.containsAnyOf(vararg t: T) = t.any(this::contains) + +// see https://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html#jls-3.10.6 +internal fun characterLiteralWithoutSingleQuotes(c: Char) = when { + c == '\b' -> "\\b" // \u0008: backspace (BS) + c == '\t' -> "\\t" // \u0009: horizontal tab (HT) + c == '\n' -> "\\n" // \u000a: linefeed (LF) + c == '\r' -> "\\r" // \u000d: carriage return (CR) + c == '\"' -> "\"" // \u0022: double quote (") + c == '\'' -> "\\'" // \u0027: single quote (') + c == '\\' -> "\\\\" // \u005c: backslash (\) + c.isIsoControl -> String.format("\\u%04x", c.code) + else -> c.toString() +} + +internal fun escapeCharacterLiterals(s: String) = buildString { + for (c in s) append(characterLiteralWithoutSingleQuotes(c)) +} + +private val Char.isIsoControl: Boolean + get() { + return this in '\u0000'..'\u001F' || this in '\u007F'..'\u009F' + } + +internal fun stringLiteralWithQuotes( + value: String, + isInsideRawString: Boolean = false, + isConstantContext: Boolean = false, +): String { + if (!isConstantContext && '\n' in value) { + val result = StringBuilder(value.length + 32) + result.append("\"\"\"\n|") + var i = 0 + while (i < value.length) { + val c = value[i] + if (value.regionMatches(i, "\"\"\"", 0, 3)) { + // Don't inadvertently end the raw string too early + result.append("\"\"\${'\"'}") + i += 2 + } else if (c == '\n') { + // Add a '|' after newlines. This pipe will be removed by trimMargin(). + result.append("\n|") + } else if (c == '$' && !isInsideRawString) { + // Escape '$' symbols with ${'$'}. + result.append("\${\'\$\'}") + } else { + result.append(c) + } + i++ + } + // If the last-emitted character wasn't a margin '|', add a blank line. This will get removed + // by trimMargin(). + if (!value.endsWith("\n")) result.append("\n") + result.append("\"\"\".trimMargin()") + return result.toString() + } else { + val result = StringBuilder(value.length + 32) + // using pre-formatted strings allows us to get away with not escaping symbols that would + // normally require escaping, e.g. "foo ${"bar"} baz" + if (isInsideRawString) result.append("\"\"\"") else result.append('"') + for (c in value) { + // Trivial case: single quote must not be escaped. + if (c == '\'') { + result.append("'") + continue + } + // Trivial case: double quotes must be escaped. + if (c == '\"' && !isInsideRawString) { + result.append("\\\"") + continue + } + // Trivial case: $ signs must be escaped. + if (c == '$' && !isInsideRawString) { + result.append("\${\'\$\'}") + continue + } + // Default case: just let character literal do its work. + result.append(if (isInsideRawString) c else characterLiteralWithoutSingleQuotes(c)) + // Need to append indent after linefeed? + } + if (isInsideRawString) result.append("\"\"\"") else result.append('"') + return result.toString() + } +} + + +// https://github.com/JetBrains/kotlin/blob/master/compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/checkers/JvmSimpleNameBacktickChecker.kt +private val ILLEGAL_CHARACTERS_TO_ESCAPE = setOf('.', ';', '[', ']', '/', '<', '>', ':', '\\') + + +private const val ALLOWED_CHARACTER = '$' + +private const val UNDERSCORE_CHARACTER = '_' + +internal val String.isKeyword get() = this in "KEYWORDS" + +internal val String.hasAllowedCharacters get() = this.any { it == ALLOWED_CHARACTER } + +internal val String.allCharactersAreUnderscore get() = this.all { it == UNDERSCORE_CHARACTER } + +private fun String.failIfEscapeInvalid() { + require(!any { it in ILLEGAL_CHARACTERS_TO_ESCAPE }) { + "Can't escape identifier $this because it contains illegal characters: " + + ILLEGAL_CHARACTERS_TO_ESCAPE.intersect(this.toSet()).joinToString("") + } +} + +internal fun String.escapeIfNecessary(validate: Boolean = true): String = escapeIfNotJavaIdentifier() + .escapeIfKeyword() + .escapeIfHasAllowedCharacters() + .escapeIfAllCharactersAreUnderscore() + .apply { if (validate) failIfEscapeInvalid() } + +private fun String.escapeIfKeyword() = if (isKeyword) "`$this`" else this + +private fun String.escapeIfHasAllowedCharacters() = if (hasAllowedCharacters) "`$this`" else this + +private fun String.escapeIfAllCharactersAreUnderscore() = if (allCharactersAreUnderscore) "`$this`" else this + +private fun String.escapeIfNotJavaIdentifier(): String { + return if (( + !Character.isJavaIdentifierStart(first()) || + drop(1).any { !Character.isJavaIdentifierPart(it) } + ) + ) { + "`$this`".replace(' ', '·') + } else { + this + } +} + +internal fun String.escapeSegmentsIfNecessary(delimiter: Char = '.') = split(delimiter) + .filter { it.isNotEmpty() } + .joinToString(delimiter.toString()) { it.escapeIfNecessary() } diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/writer/CodeWriter.kt b/src/main/kotlin/net/theevilreaper/dartpoet/writer/CodeWriter.kt deleted file mode 100644 index f8cae5f5..00000000 --- a/src/main/kotlin/net/theevilreaper/dartpoet/writer/CodeWriter.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.theevilreaper.dartpoet.writer - -import java.io.Closeable - -class CodeWriter( - out: Appendable -) : Closeable { - override fun close() { - } - - -} \ No newline at end of file diff --git a/src/main/kotlin/net/theevilreaper/dartpoet/writer/FragmentPart.kt b/src/main/kotlin/net/theevilreaper/dartpoet/writer/FragmentPart.kt deleted file mode 100644 index e9b4f429..00000000 --- a/src/main/kotlin/net/theevilreaper/dartpoet/writer/FragmentPart.kt +++ /dev/null @@ -1,27 +0,0 @@ -package net.theevilreaper.dartpoet.writer - -enum class FragmentPart( - val part: String, - val identifier: Char -) { - - NAMED("%N", 'N'), - LITERAL("%L", 'L'), - STRING("%S", 'S'), - STRING_NOT_ESCAPED("%P", 'P'), - MEMBER("%M", 'M'); - - companion object { - /** - * Converts a string which contains a part from a fragment into a [FragmentPart] value. - * @param partString the string which contains a string part - */ - fun mapToFragmentPart(partString: String): FragmentPart? { - return values().firstOrNull { it.part === partString } - } - - fun mapByIdentifier(identifier: Char): FragmentPart? { - return values().firstOrNull { it.identifier == identifier } - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/DartClassBuilderTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/DartClassBuilderTest.kt deleted file mode 100644 index ec41f0f1..00000000 --- a/src/test/kotlin/net/theevilreaper/dartpoet/DartClassBuilderTest.kt +++ /dev/null @@ -1,63 +0,0 @@ -package net.theevilreaper.dartpoet - -import com.google.common.truth.Truth.assertThat -import net.theevilreaper.dartpoet.annotation.AnnotationSpec -import net.theevilreaper.dartpoet.clazz.DartClassSpec -import net.theevilreaper.dartpoet.property.DartPropertySpec -import org.junit.Test - -class DartClassBuilderTest { - - private val className: String = "DartClass" - - @Test - fun `test simple class creation`() { - val clazz = - DartClassSpec.builder(className).annotation(AnnotationSpec()).modifier { DartModifier.FINAL }.build() - - assertThat(clazz.toString()).isEqualTo(""" - public class $className { - } - """.trimIndent()) - } - - @Test - fun `test enum class`() { - val clazz = DartClassSpec.enumClass(className).build() - assertThat(clazz.toString()).isEqualTo(""" - enum $className { - } - """.trimIndent()) - } - - @Test - fun `test mixin class`() { - val clazz = DartClassSpec.mixinClass(className).build() - assertThat(clazz.toString()).isEqualTo(""" - mixin $className { - } - """.trimIndent() - ) - } - - @Test - fun `test class with a parameter`() { - val clazz = DartClassSpec.builder(className) - .property(DartPropertySpec - .builder("name", "String") - .nullable(false) - .build() - ) - .build() - - assertThat(clazz.toString()).isEqualTo( - """ - public class $className { - - String name; - } - - """.trimIndent() - ) - } -} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/DartFileImportTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/DartFileImportTest.kt new file mode 100644 index 00000000..d301d867 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/DartFileImportTest.kt @@ -0,0 +1,107 @@ +package net.theevilreaper.dartpoet + +import com.google.common.truth.Truth.assertThat +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import net.theevilreaper.dartpoet.clazz.ClassSpec +import net.theevilreaper.dartpoet.code.buildCodeBlock +import net.theevilreaper.dartpoet.directive.CastType +import net.theevilreaper.dartpoet.directive.DirectiveFactory +import net.theevilreaper.dartpoet.directive.DirectiveType +import net.theevilreaper.dartpoet.function.FunctionSpec +import net.theevilreaper.dartpoet.type.ClassName +import net.theevilreaper.dartpoet.type.ParameterizedTypeName.Companion.parameterizedBy +import kotlin.test.Test + +class DartFileImportTest { + + @Test + fun `test file write with sorted imports`() { + val model = ClassName("sound_model") + val appState = ClassName("AppState") + val reduxAction = ClassName("ReduxAction").parameterizedBy(appState) + val dartFile = DartFile.builder("Test") + .directives( + DirectiveFactory.create(DirectiveType.IMPORT, "dart:io"), + DirectiveFactory.create(DirectiveType.IMPORT, "dart:math"), + DirectiveFactory.create(DirectiveType.IMPORT, "async_redux/async_redux.dart"), + DirectiveFactory.create(DirectiveType.IMPORT, "model/${model.name}.dart"), + ) + .type( + ClassSpec.builder("TestAction") + .endWithNewLine(true) + .superClass(reduxAction, InheritKeyword.EXTENDS) + .function( + FunctionSpec.builder("reduce") + .annotations( + AnnotationSpec.builder("override") + .build() + ) + .async(true) + .returns(appState) + .addCode( + buildCodeBlock { + addStatement("var models = [];") + add("return state.copyWith(sounds: models);") + } + ) + .build() + ) + ) + .build() + assertThat(dartFile.toString()).isEqualTo( + """ + import 'dart:io'; + import 'dart:math'; + + import 'package:async_redux/async_redux.dart'; + import 'package:model/sound_model.dart'; + + class TestAction extends ReduxAction { + + @override + Future reduce() async { + var models = []; + return state.copyWith(sounds: models); + } + } + + """.trimIndent() + ) + } + + @Test + fun `test directives with an export directive`() { + val classFile = DartFile.builder("House") + .directives( + DirectiveFactory.create(DirectiveType.IMPORT, "dart:io"), + DirectiveFactory.create(DirectiveType.IMPORT, "door"), + DirectiveFactory.create(DirectiveType.PART, "house_part.dart"), + DirectiveFactory.create(DirectiveType.EXPORT, "garden.dart", CastType.SHOW, "garden"), + ) + .type( + ClassSpec.builder("House") + .annotation( + AnnotationSpec.builder("immutable") + .build() + ) + .endWithNewLine(true) + .build() + ) + .build() + assertThat(classFile.toString()).isEqualTo( + """ + import 'dart:io'; + + import 'package:door.dart'; + + export 'garden.dart' show garden; + + part 'house_part.dart'; + + @immutable + class House {} + + """.trimIndent() + ) + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/DartFileTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/DartFileTest.kt new file mode 100644 index 00000000..5ee1dbbb --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/DartFileTest.kt @@ -0,0 +1,379 @@ +package net.theevilreaper.dartpoet + +import com.google.common.truth.Truth.assertThat +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import net.theevilreaper.dartpoet.clazz.ClassSpec +import net.theevilreaper.dartpoet.code.buildCodeBlock +import net.theevilreaper.dartpoet.directive.CastType +import net.theevilreaper.dartpoet.directive.DirectiveFactory +import net.theevilreaper.dartpoet.directive.DirectiveType +import net.theevilreaper.dartpoet.function.FunctionSpec +import net.theevilreaper.dartpoet.function.constructor.ConstructorSpec +import net.theevilreaper.dartpoet.function.typedef.TypeDefSpec +import net.theevilreaper.dartpoet.parameter.ParameterSpec +import net.theevilreaper.dartpoet.property.PropertySpec +import net.theevilreaper.dartpoet.property.consts.ConstantPropertySpec +import net.theevilreaper.dartpoet.type.ClassName +import net.theevilreaper.dartpoet.type.DYNAMIC +import net.theevilreaper.dartpoet.type.ParameterizedTypeName.Companion.parameterizedBy +import net.theevilreaper.dartpoet.type.asTypeName +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import kotlin.test.assertContentEquals + +class DartFileTest { + + @Test + fun `test indent set`() { + assertThrows( + IllegalStateException::class.java, + { DartFile.builder("Test").indent("") }, + "The indent can't be empty" + ) + assertThrows( + IllegalStateException::class.java, + { DartFile.builder("Test").indent { " 123AB" } }, + "The indent can't be empty" + ) + } + + @Test + fun `test spec to builder conversation`() { + val dartFileSpec = DartFile.builder("TestClass") + .indent(" ") + .annotation(AnnotationSpec.builder("ignore").build()) + .build() + val specAsBuilder = dartFileSpec.toBuilder() + assertEquals(dartFileSpec.name, specAsBuilder.name) + assertEquals(dartFileSpec.indent, specAsBuilder.indent) + assertContentEquals(dartFileSpec.annotations, specAsBuilder.annotations) + } + + @Test + fun `write test model with freezed`() { + val freezedMixing = ClassName("_${'$'}VersionModel") + val versionFreezedClass = ClassSpec.builder("VersionModel") + .superClass(freezedMixing, InheritKeyword.MIXIN) + .annotation { AnnotationSpec.builder("freezed").build() } + .constructor { + ConstructorSpec.builder("VersionModel") + .asFactory(true) + .modifier(DartModifier.CONST) + .parameter { + ParameterSpec.builder("version", String::class) + .named(true) + .annotations( + AnnotationSpec.builder("JsonKey") + .content("name: %C", "version").build(), + AnnotationSpec.builder("Default") + .content("%C", "1.0.0").build() + ) + .build() + } + .build() + } + .constructor { + ConstructorSpec.named("VersionModel", "fromJson") + .lambda(true) + .asFactory(true) + .parameter( + ParameterSpec.builder( + "json", + Map::class.parameterizedBy(String::class.asTypeName(), DYNAMIC) + ).build() + ) + .addCode("%L", "_${"$"}VersionModelFromJson(json);") + .build() + } + val versionFile = DartFile.builder("version.dart") + .directives( + DirectiveFactory.create(DirectiveType.IMPORT, "freezed_annotation/freezed_annotation.dart"), + DirectiveFactory.create(DirectiveType.PART, "version.freezed.dart"), + DirectiveFactory.create(DirectiveType.PART, "version.g.dart") + ) + .type( + versionFreezedClass + ) + .build() + assertThat(versionFile.toString()).isEqualTo( + """ + import 'package:freezed_annotation/freezed_annotation.dart'; + + part 'version.freezed.dart'; + part 'version.g.dart'; + + @freezed + class VersionModel with _${'$'}VersionModel { + + const factory VersionModel({ + @JsonKey(name: 'version')@Default('1.0.0') String version + }) = _VersionModel; + + factory VersionModel.fromJson(Map json) => + _${'$'}VersionModelFromJson(json); + + } + """.trimIndent() + ) + } + + @Test + fun `test library write`() { + val libClass = DartFile.builder("testLib") + .type( + ClassSpec.anonymousClassBuilder() + .typedef( + TypeDefSpec.builder("JsonMap") + .returns(Map::class.parameterizedBy(String::class.asTypeName(), DYNAMIC)) + .build() + + ) + .build() + ) + .directives( + DirectiveFactory.create(DirectiveType.IMPORT, "dart:html"), + DirectiveFactory.createLib("testLib"), + DirectiveFactory.create(DirectiveType.IMPORT, "dart:math", CastType.AS, "math"), + ) + .build() + assertThat(libClass.toString()).isEqualTo( + """ + library testLib; + + import 'dart:html'; + import 'dart:math' as math; + + typedef JsonMap = Map; + + """.trimIndent() + ) + } + + @Test + fun `test api handler write`() { + val className = "DefectApi" + val apiClassName = ClassName("ApiClient") + val apiClient = "ApiClient" + + val handlerApiClass = ClassSpec.builder(className) + .property(PropertySpec.builder(apiClient.replaceFirstChar { it.lowercase() }, apiClassName) + .modifier { DartModifier.FINAL } + .build() + ) + .constructor( + ConstructorSpec.builder(className) + .parameter( + ParameterSpec.builder(apiClient.replaceFirstChar { it.lowercase() }, apiClassName).build() + ) + .addCode(buildCodeBlock { + add( + "%L = %L", + apiClient.replaceFirstChar { it.lowercase() }, + apiClient.replaceFirstChar { it.lowercase() }) + }) + .build() + ) + .function( + FunctionSpec.builder("getByID") + .async(true) + .returns(ClassName("DefectDTO")) + .parameter(ParameterSpec.builder("id", Int::class).build()) + .addCode(buildCodeBlock { + addStatement("final queryParams = %L;", "{}") + addStatement("final baseUri = Uri.parse(apiClient.baseUrl);") + addStatement("final uri = baseUri.replace(queryParameters: queryParameters, path: '\${baseUri.path}/defect/\$id/');") + addStatement("return await apiClient.dio.getUri(") + indent() + addStatement("uri,") + unindent() + addStatement(").then((response) {") + indent() + addStatement("return DefectDTO.from(response.data!);") + unindent() + addStatement("});") + + }) + .build() + ) + .build() + + val file = DartFile.builder("${className}Handler") + .directive(DirectiveFactory.createLib("testLibrary", true)) + .type(handlerApiClass) + .build() + assertThat(file.toString()).isEqualTo( + """ + part of testLibrary; + + class DefectApi { + + final ApiClient apiClient; + + DefectApi(ApiClient apiClient): apiClient = apiClient; + + Future getByID(int id) async { + final queryParams = {}; + final baseUri = Uri.parse(apiClient.baseUrl); + final uri = baseUri.replace(queryParameters: queryParameters, path: '${"$"}{baseUri.path}/defect/${"$"}id/'); + return await apiClient.dio.getUri( + uri, + ).then((response) { + return DefectDTO.from(response.data!); + }); + + } + } + """.trimIndent() + ) + } + + @Test + fun `test model class write`() { + val name = "HousePart" + val houseClass = ClassName(name) + val serializer = "standardSerializers" + val serializerClass = ClassName("Built<$name, ${name}Builder>") + val modelClass = ClassSpec.abstractClass(name) + .superClass(serializerClass, InheritKeyword.IMPLEMENTS) + .function( + FunctionSpec.builder("serializer") + .returns(ClassName("Serializer<$name>")) + .lambda(true) + .getter(true) + .modifier(DartModifier.STATIC) + .addCode("%L", "_\$${name}Serializer;") + .build() + ) + .function( + FunctionSpec.builder("fromJson") + .lambda(true) + .returns(houseClass) + .modifier(DartModifier.STATIC) + .parameter(ParameterSpec.builder("json", DYNAMIC).build()) + .addCode(buildCodeBlock { + add("%L.deserialize(json);", serializer) + }) + .build() + ) + .function( + FunctionSpec.builder("toJson") + .lambda(true) + .returns(DYNAMIC) + .addCode(buildCodeBlock { + add("%L.serialize(this);", "standardSerializers") + }) + .build() + ) + .build() + assertThat(modelClass.toString()).isEqualTo( + """ + abstract class $name implements Built<$name, ${name}Builder> { + + static Serializer<$name> get serializer => _$${name}Serializer; + + static $name fromJson(dynamic json) => $serializer.deserialize(json); + + dynamic toJson() => $serializer.serialize(this); + } + """.trimIndent() + ) + } + + @Test + fun `test class write with constant values`() { + val name = "environment" + val classFile = DartFile.builder(name) + .directive(DirectiveFactory.create(DirectiveType.IMPORT, "dart:html")) + .constants( + ConstantPropertySpec.fileConst("typeLive").initWith("1").build(), + ConstantPropertySpec.fileConst("typeTest").initWith("10").build(), + ConstantPropertySpec.fileConst("typeDev").initWith("100").build(), + ) + .type( + ClassSpec.builder(name.replaceFirstChar { it.uppercase() }) + .annotation(AnnotationSpec.builder("freezed").build()) + ) + .build() + assertThat(classFile.toString()).isEqualTo( + """ + import 'dart:html'; + + const typeLive = 1; + const typeTest = 10; + const typeDev = 100; + + @freezed + class Environment {} + """.trimIndent() + ) + } + + @Test + fun `test class with comment`() { + val clazz = DartFile.builder("test") + .doc("Hallo") + .doc("This is a [%L]", "Test") + .type( + ClassSpec.builder("Test") + ) + .build() + assertThat(clazz.toString()).isEqualTo( + """ + /// Hallo + /// This is a [Test] + class Test {} + """.trimIndent() + ) + } + + @Test + fun `test class with a bunch of comments`() { + val spec = ClassSpec.builder("TestModel") + .property { + PropertySpec.builder("name", String::class) + .docs("Property comment") + .build() + } + .constructor( + ConstructorSpec.builder("TestModel") + .parameter(ParameterSpec.builder("name").build()) + .doc("Good comment") + .build() + ) + .function( + FunctionSpec.builder("getName") + .doc("Returns the given name from the object") + .returns(String::class) + .addCode(buildCodeBlock { + add("return name;") + }) + .build() + ) + val file = DartFile.builder("test_model") + .type(spec.build()) + .doc("Class documentation is good") + .doc("And its working") + .build() + + assertThat(file.toString()).isEqualTo( + """ + /// Class documentation is good + /// And its working + class TestModel { + + /// Property comment + String name; + + /// Good comment + TestModel(this.name); + + /// Returns the given name from the object + String getName() { + return name; + } + } + """.trimIndent() + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/DartImportTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/DartImportTest.kt deleted file mode 100644 index dc357012..00000000 --- a/src/test/kotlin/net/theevilreaper/dartpoet/DartImportTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -package net.theevilreaper.dartpoet - -import junit.framework.TestCase.assertEquals -import net.theevilreaper.dartpoet.import.DartImport -import net.theevilreaper.dartpoet.import.ImportCastType -import org.junit.Test - -class DartImportTest { - - private val packageImport = "import 'package:flutter/material.dart';" - private val modelImport = "import '../../model/item_model.dart' as item;" - private val lazyCastImport = "import '../../model/item_model.dart' deferred as item;" - private val hideCastImport = "import '../../model/item_model.dart' hide item;" - private val showCastImport = "import '../../model/item_model.dart' show item;" - - private val castValue = "item" - private val testImport = "../../model/item_model.dart" - - @Test - fun `test package import`() { - val import = DartImport("flutter/material.dart") - assertEquals(packageImport, import.toString()) - } - - @Test - fun `test package with cast`() { - val import = DartImport(testImport, ImportCastType.AS,castValue) - assertEquals(modelImport, import.toString()) - } - - @Test - fun `test lazy import`() { - val import = DartImport(testImport, ImportCastType.DEFERRED,castValue) - assertEquals(lazyCastImport, import.toString()) - } - - @Test - fun `test import with hide`() { - val import = DartImport(testImport, ImportCastType.HIDE, castValue) - assertEquals(hideCastImport, import.toString()) - } - - @Test - fun `test import with show`() { - val import = DartImport(testImport, ImportCastType.SHOW, castValue) - assertEquals(showCastImport, import.toString()) - } -} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/DartModifierTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/DartModifierTest.kt index 6453e5ca..8a35eb62 100644 --- a/src/test/kotlin/net/theevilreaper/dartpoet/DartModifierTest.kt +++ b/src/test/kotlin/net/theevilreaper/dartpoet/DartModifierTest.kt @@ -1,6 +1,6 @@ package net.theevilreaper.dartpoet -import org.junit.Test +import org.junit.jupiter.api.Test import kotlin.test.assertFalse import kotlin.test.assertTrue diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/DartPropertySpecTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/DartPropertySpecTest.kt deleted file mode 100644 index 025ca2b3..00000000 --- a/src/test/kotlin/net/theevilreaper/dartpoet/DartPropertySpecTest.kt +++ /dev/null @@ -1,14 +0,0 @@ -package net.theevilreaper.dartpoet - -import com.google.common.truth.Truth.assertThat -import net.theevilreaper.dartpoet.property.DartPropertySpec -import org.junit.Test - -class DartPropertySpecTest { - - @Test - fun `test logic`() { - val property = DartPropertySpec.builder("test", "String").nullable(true).build() - assertThat(property.toString()).isEqualTo("String? test;") - } -} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/PartImportTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/PartImportTest.kt deleted file mode 100644 index a74b709f..00000000 --- a/src/test/kotlin/net/theevilreaper/dartpoet/PartImportTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package net.theevilreaper.dartpoet - -import net.theevilreaper.dartpoet.import.PartImport -import org.junit.Test -import org.junit.jupiter.api.Assertions.* - -class PartImportTest { - - private val expectedImport = "part 'item_model.freezed.dart';" - - @Test - fun `create part import`() { - val partImport = PartImport("item_model.freezed.dart") - assertEquals(expectedImport, partImport.toString()) - } -} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/annotation/AnnotationSpecTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/annotation/AnnotationSpecTest.kt new file mode 100644 index 00000000..fd5d0b25 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/annotation/AnnotationSpecTest.kt @@ -0,0 +1,44 @@ +package net.theevilreaper.dartpoet.annotation + +import net.theevilreaper.dartpoet.type.ClassName +import net.theevilreaper.dartpoet.type.DEPRECATED +import net.theevilreaper.dartpoet.type.OVERRIDE +import net.theevilreaper.dartpoet.type.PRAGMA +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class AnnotationSpecTest { + + companion object { + + @JvmStatic + private fun testSimpleAnnotations() = Stream.of( + Arguments.of(OVERRIDE, "@override"), + Arguments.of(DEPRECATED, "@deprecated"), + Arguments.of(PRAGMA, "@pragma"), + Arguments.of(AnnotationSpec.builder(Override::class).build(), "@Override") + ) + } + + @ParameterizedTest + @MethodSource("testSimpleAnnotations") + fun `test simple annotations`(annotation: AnnotationSpec, expected: String) { + assertEquals(expected, annotation.toString()) + } + + @Test + fun `test toBuilder function`() { + val className = ClassName("ignore") + val annotationSpec = AnnotationSpec.builder(className) + .content("ignore", "true") + .build() + val specAsBuilder = annotationSpec.toBuilder() + assertEquals(annotationSpec.typeName, className) + assertContentEquals(annotationSpec.content, specAsBuilder.content) + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/classTypes/AbstractClassTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/classTypes/AbstractClassTest.kt new file mode 100644 index 00000000..8f793c9c --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/classTypes/AbstractClassTest.kt @@ -0,0 +1,57 @@ +package net.theevilreaper.dartpoet.classTypes + +import com.google.common.truth.Truth.* +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import net.theevilreaper.dartpoet.clazz.ClassSpec +import net.theevilreaper.dartpoet.function.FunctionSpec +import net.theevilreaper.dartpoet.parameter.ParameterSpec +import net.theevilreaper.dartpoet.type.ClassName +import org.junit.jupiter.api.Test + +class AbstractClassTest { + + @Test + fun `test simple abstract class`() { + val abstractClass = ClassSpec.abstractClass("DatabaseHandler") + .endWithNewLine(true) + .function( + FunctionSpec.builder("getByID") + .returns(ClassName("TestModel")) + .parameter(ParameterSpec.builder("id", Int::class).build()) + .build() + ) + .function(FunctionSpec.builder("test").build()) + .build() + + assertThat(abstractClass.toString()).isEqualTo( + """ + abstract class DatabaseHandler { + + TestModel getByID(int id); + + void test(); + } + + """.trimIndent() + ) + } + + @Test + fun `test abstract class with annotation`() { + val abstractClass = ClassSpec.abstractClass("Test") + .annotation( + AnnotationSpec.builder("abc").build() + ) + .function(FunctionSpec.builder("test").build()) + .build() + assertThat(abstractClass.toString()).isEqualTo( + """ + @abc + abstract class Test { + + void test(); + } + """.trimIndent() + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/classTypes/EnumClassTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/classTypes/EnumClassTest.kt new file mode 100644 index 00000000..a1a26952 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/classTypes/EnumClassTest.kt @@ -0,0 +1,154 @@ +package net.theevilreaper.dartpoet.classTypes + +import com.google.common.truth.Truth.assertThat +import net.theevilreaper.dartpoet.DartFile +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.clazz.ClassSpec +import net.theevilreaper.dartpoet.enum.EnumPropertySpec +import net.theevilreaper.dartpoet.function.constructor.ConstructorSpec +import net.theevilreaper.dartpoet.parameter.ParameterSpec +import net.theevilreaper.dartpoet.property.PropertySpec +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream +import kotlin.test.assertEquals + +class EnumClassTest { + + companion object { + + @JvmStatic + private fun invalidEnums() = Stream.of( + Arguments.of( + { + ClassSpec.enumClass("TestEnum") + .property( + PropertySpec.builder("name", String::class).build() + ) + .build() + }, + "A enum requires at least one enum property" + ), + Arguments.of( + { + ClassSpec.enumClass("TestEnum") + .enumProperties( + EnumPropertySpec.builder("test") + .parameter("%C", "Test") + .parameter("%L", "10") + .build() + ) + .property(PropertySpec.builder("name", String::class).build()) + .constructor( + ConstructorSpec.builder("TestEnum") + .parameter(ParameterSpec.builder("name").build()) + .build() + ) + .build() + }, + "The entries from the enum property must have the same size" + ) + ) + } + + @ParameterizedTest + @MethodSource("invalidEnums") + fun `test invalid enum creation`(classSpec: () -> Unit, message: String) { + val exception = assertThrows { classSpec() } + assertEquals(IllegalStateException::class, exception::class) + assertEquals(message, exception.message) + } + + @Test + fun `test invalid enum creation`() { + val enumClass = ClassSpec.enumClass("TestEnum") + .property(PropertySpec.builder("name", String::class).build()) + .constructor( + ConstructorSpec.builder("TestEnum") + .parameter(ParameterSpec.builder("name").build()) + .build() + ) + val exception = assertThrows { enumClass.build() } + assertEquals(IllegalStateException::class, exception::class) + assertEquals("A enum requires at least one enum property", exception.message) + } + + @Test + fun `test enum class write`() { + val enumClass = DartFile.builder("navigation_entry") + .type( + ClassSpec.enumClass("NavigationEntry") + .properties( + PropertySpec.builder("name", String::class) + .modifier { DartModifier.FINAL }.build(), + PropertySpec.builder("route", String::class) + .modifier { DartModifier.FINAL }.build() + + ) + .enumProperties( + EnumPropertySpec.builder("dashboard") + .parameter("%C", "Dashboard") + .parameter("%C", "/dashboard") + .build(), + EnumPropertySpec.builder("build") + .parameter("%C", "Build") + .parameter("%C", "/build") + .build() + ) + .constructor( + ConstructorSpec.builder("NavigationEntry") + .modifier(DartModifier.CONST) + .parameters( + ParameterSpec.builder("name").build(), + ParameterSpec.builder("route").build() + ) + .build() + ) + .build() + ) + .build() + assertThat(enumClass.toString()).isEqualTo( + """ + enum NavigationEntry { + + dashboard('Dashboard', '/dashboard'), + build('Build', '/build'); + + final String name; + final String route; + + const NavigationEntry(this.name, this.route); + + } + """.trimIndent() + ) + } + + @Test + fun d() { + val enumClass = ClassSpec.enumClass("TestEnum") + .enumProperties( + EnumPropertySpec.builder("test") + .parameter("%C", "Test") + .build() + ) + .properties( + PropertySpec.builder("name", String::class).build() + ) + .constructor( + ConstructorSpec.builder("TestEnum") + .parameter(ParameterSpec.builder("name").build()) + .build() + ) + .build() + val file = DartFile.builder("test") + .type(enumClass) + .build() + file.write(System.out) + } +} + + diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/code/CodeBlockTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/code/CodeBlockTest.kt new file mode 100644 index 00000000..6fd47686 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/code/CodeBlockTest.kt @@ -0,0 +1,36 @@ +package net.theevilreaper.dartpoet.code + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test + +//TODO: Add tests +class CodeBlockTest { + + @Test + fun `test string write`() { + val block = CodeBlock.builder().add("Test %S", "!!!").build() + assertThat(block.toString()).isEqualTo("Test \"!!!\"") + } + + @Test + fun `test literal write`() { + val block = CodeBlock.builder().add("The %L is a lie", "cake").build() + assertThat(block.toString()).isEqualTo("The cake is a lie") + } + + @Test + fun `test simple if statement`() { + val block = CodeBlock.builder() + .beginControlFlow("if (value == null)") + .addStatement("return null;") + .endControlFlow() + .build() + assertThat(block.toString().trim()).isEqualTo( + """ + if (value == null) { + return null; + } + """.trimIndent() + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/code/LineWrapperTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/code/LineWrapperTest.kt new file mode 100644 index 00000000..c45d1fa3 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/code/LineWrapperTest.kt @@ -0,0 +1,56 @@ +package net.theevilreaper.dartpoet.code + +import com.google.common.truth.Truth.assertThat +import net.theevilreaper.dartpoet.util.DEFAULT_INDENT +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class LineWrapperTest { + + @Test + fun `test non wrapping appending`() { + val builder = StringBuilder() + val appender = LineWrapper(builder, DEFAULT_INDENT, 10) + appender.appendNonWrapping("This is a") + appender.appendNonWrapping(" test") + appender.close() + assertEquals("This is a test", builder.toString()) + } + + @Test + fun `test append string write`() { + val builder = StringBuilder() + val appender = LineWrapper(builder, "", 100) + appender.append("Test") + appender.close() + assertEquals("Test", builder.toString()) + } + + @Test + fun `test manual line breaking`() { + val builder = StringBuilder() + val appender = LineWrapper(builder, " ", 100) + appender.append("Line\nBreak", indentLevel = 1) + appender.close() + assertThat(builder.toString()).isEqualTo( + """ + Line + Break + """.trimIndent() + ) + } + + @Test + fun `test line break`() { + val builder = StringBuffer() + val appender = LineWrapper(builder, " ", 6) + appender.append("Second Test", indentLevel = 1) + appender.close() + assertThat(builder.toString()).isEqualTo( + """ + Second + Test + """.trimIndent() + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/AnnotationWriterTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/AnnotationWriterTest.kt new file mode 100644 index 00000000..263dbd02 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/AnnotationWriterTest.kt @@ -0,0 +1,22 @@ +package net.theevilreaper.dartpoet.code.writer + +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class AnnotationWriterTest { + + @Test + fun `test annotation write without content`() { + val annotationSpec = AnnotationSpec.builder("jsonIgnore").build() + assertEquals("@jsonIgnore", annotationSpec.toString()) + } + + @Test + fun `test annotation write with content`() { + val annotationSpec = AnnotationSpec.builder("JsonKey") + .content("%C", "Test").build() + + assertEquals("@JsonKey('Test')", annotationSpec.toString()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/ClassWriterTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/ClassWriterTest.kt new file mode 100644 index 00000000..c66528d7 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/ClassWriterTest.kt @@ -0,0 +1,66 @@ +package net.theevilreaper.dartpoet.code.writer + +import com.google.common.truth.Truth.assertThat +import net.theevilreaper.dartpoet.clazz.ClassSpec +import net.theevilreaper.dartpoet.property.consts.ConstantPropertySpec +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class ClassWriterTest { + + companion object { + + @JvmStatic + private fun simpleClasses() = Stream.of( + Arguments.of(ClassSpec.builder("Test").build(), "class Test {}"), + Arguments.of(ClassSpec.mixinClass("Test").build(), "mixin Test {}"), + Arguments.of( + ClassSpec.builder("Model").endWithNewLine(true).build(), + """ + class Model {} + + """.trimIndent() + ), + Arguments.of( + ClassSpec.abstractClass("DatabaseHandler").endWithNewLine(true).build(), + """ + abstract class DatabaseHandler {} + + """.trimIndent() + ) + ) + } + + @ParameterizedTest + @MethodSource("simpleClasses") + fun `test simple classes`(classSpec: ClassSpec, expected: String) { + assertThat(classSpec.toString()).isEqualTo(expected) + } + + @Test + fun `test class writing with some constants`() { + val clazz = ClassSpec.builder("TestClass") + .constants( + ConstantPropertySpec.classConst("test", String::class) + .initWith("%C", "Test") + .build(), + ConstantPropertySpec.classConst("maxId", Int::class) + .initWith("%L", "100") + .build(), + ) + .build() + assertThat(clazz.toString()).isEqualTo( + """ + class TestClass { + + static const String test = 'Test'; + static const int maxId = 100; + + } + """.trimIndent() + ) + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/ConstantPropertyWriterTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/ConstantPropertyWriterTest.kt new file mode 100644 index 00000000..93d95f8b --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/ConstantPropertyWriterTest.kt @@ -0,0 +1,95 @@ +package net.theevilreaper.dartpoet.code.writer + +import com.google.common.truth.Truth.assertThat +import net.theevilreaper.dartpoet.property.consts.ConstantPropertySpec +import net.theevilreaper.dartpoet.type.ClassName +import net.theevilreaper.dartpoet.type.ParameterizedTypeName.Companion.parameterizedBy +import net.theevilreaper.dartpoet.type.asClassName +import net.theevilreaper.dartpoet.type.asTypeName +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class ConstantPropertyWriterTest { + + companion object { + + @JvmStatic + private fun testFileConstantWriting() = Stream.of( + Arguments.of( + "const String test = 'Test';", + { ConstantPropertySpec.fileConst("test", String::class).initWith("%C", "Test").build() } + ), + Arguments.of( + "const maxId = 100;", + { ConstantPropertySpec.fileConst("maxId").initWith("%L", "100").build() } + ), + Arguments.of( + "const List strings = [];", + { + ConstantPropertySpec.fileConst( + "strings", + List::class.asClassName().parameterizedBy(String::class.asTypeName()) + ).initWith("[]").build() + } + ), + Arguments.of( + "const TestModel model = TestModel();", + { + ConstantPropertySpec.fileConst("model", ClassName("TestModel")).initWith("%L", "TestModel()") + .build() + } + ) + ) + + @JvmStatic + private fun testClassConstWriting() = Stream.of( + Arguments.of( + "static const String test = 'Test';", + { ConstantPropertySpec.classConst("test", String::class).initWith("%C", "Test").build() } + ), + Arguments.of( + "static const maxId = 100;", + { ConstantPropertySpec.classConst("maxId").initWith("%L", "100").build() } + ), + Arguments.of( + "static const List strings = [];", + { + ConstantPropertySpec.classConst( + "strings", + List::class.asClassName().parameterizedBy(String::class.asTypeName()) + ).initWith("[]").build() + } + ), + Arguments.of( + "static const TestModel model = TestModel();", + { + ConstantPropertySpec.classConst("model", ClassName("TestModel")).initWith("%L", "TestModel()") + .build() + } + ), + Arguments.of( + "static const int _test = 1;", + { + ConstantPropertySpec.classConst("test", Int::class) + .initWith("%L", "1") + .private(true) + .build() + } + ) + ) + } + + @ParameterizedTest + @MethodSource("testFileConstantWriting") + fun `test basic file const creation`(expected: String, block: () -> ConstantPropertySpec) { + assertThat(block().toString()).isEqualTo(expected) + } + + @ParameterizedTest + @MethodSource("testClassConstWriting") + fun `test basic class const creation`(expected: String, block: () -> ConstantPropertySpec) { + assertThat(block().toString()).isEqualTo(expected) + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/ConstructorWriterTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/ConstructorWriterTest.kt new file mode 100644 index 00000000..65ff9e06 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/ConstructorWriterTest.kt @@ -0,0 +1,137 @@ +package net.theevilreaper.dartpoet.code.writer + +import com.google.common.truth.Truth.* +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.code.CodeBlock +import net.theevilreaper.dartpoet.function.constructor.ConstructorSpec +import net.theevilreaper.dartpoet.parameter.ParameterSpec +import org.junit.jupiter.api.Test + +class ConstructorWriterTest { + + @Test + fun `test constructor write without any special parameter`() { + val constructor = ConstructorSpec.builder("Car") + .parameters( + ParameterSpec.builder("maker").build(), + ParameterSpec.builder("model").build(), + ParameterSpec.builder("yearMade").build(), + ParameterSpec.builder("hasABS").build() + ) + .build() + assertThat(constructor.toString()).isEqualTo( + """ + Car(this.maker, this.model, this.yearMade, this.hasABS); + """.trimIndent() + ) + } + + @Test + fun `test named constructor write without any special parameter`() { + val constructor = ConstructorSpec.named("Car", "withoutABS") + .parameters( + ParameterSpec.builder("maker").build(), + ParameterSpec.builder("model").build(), + ParameterSpec.builder("yearMade").build(), + ) + .build() + assertThat(constructor.toString()).isEqualTo( + """ + Car.withoutABS(this.maker, this.model, this.yearMade); + """.trimIndent() + ) + } + + @Test + fun `test named constructor2 write without any special parameter`() { + val constructor = ConstructorSpec.named("Car", "withoutABS") + .parameters( + ParameterSpec.builder("maker").build(), + ParameterSpec.builder("model").build(), + ParameterSpec.builder("yearMade").build(), + ) + .addCode( + CodeBlock.of( + "hasABS = false" + ) + ) + .build() + assertThat(constructor.toString()).isEqualTo( + """ + Car.withoutABS(this.maker, this.model, this.yearMade): hasABS = false; + """.trimIndent() + ) + } + + @Test + fun `test const constructor without any parameter which has special properties`() { + val constructor = ConstructorSpec.builder("Car") + .modifier(DartModifier.CONST) + .parameters( + ParameterSpec.builder("maker").build(), + ParameterSpec.builder("model").build(), + ParameterSpec.builder("yearMade").build(), + ) + .build() + assertThat(constructor.toString()).isEqualTo( + """ + const Car(this.maker, this.model, this.yearMade); + """.trimIndent() + ) + } + + @Test + fun `test constructor with required and named parameters`() { + val constructor = ConstructorSpec.builder("Car") + .parameters( + ParameterSpec.builder("maker").required().build(), + ParameterSpec.builder("model").named(true).build(), + ParameterSpec.builder("yearMade").required().build(), + ) + .build() + assertThat(constructor.toString()).isEqualTo( + """ + Car({ + required this.maker, + this.model, + required this.yearMade + }); + """.trimIndent() + ) + } + + @Test + fun `test constructor with named and variable with initializer`() { + val constructor = ConstructorSpec.builder("Item") + .parameters( + ParameterSpec.builder("name").required().build(), + ParameterSpec.builder("id").initializer("%L", 10L).build(), + ParameterSpec.builder("amount").required().build() + ) + .build() + assertThat(constructor.toString()).isEqualTo( + """ + Item(this.id = 10, + { + required this.name, + required this.amount + }); + """.trimIndent() + ) + } + + @Test + fun `test constructor with documentation or comments`() { + val constructor = ConstructorSpec.builder("Item") + .doc("Creates a new item object") + .parameters( + ParameterSpec.builder("name").build()) + .build() + assertThat(constructor.toString()).isEqualTo( + """ + /// Creates a new item object + Item(this.name); + """.trimIndent() + ) + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/EnumPropertyWriterTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/EnumPropertyWriterTest.kt new file mode 100644 index 00000000..3b45ca54 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/EnumPropertyWriterTest.kt @@ -0,0 +1,77 @@ +package net.theevilreaper.dartpoet.code.writer + +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import net.theevilreaper.dartpoet.enum.EnumPropertySpec +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class EnumPropertyWriterTest { + + companion object { + + @JvmStatic + private fun properties() = Stream.of( + Arguments.of(EnumPropertySpec.builder("test").generic(String::class).build(), "test"), + Arguments.of( + EnumPropertySpec.builder("test") + .parameter("%C", "/dash") + .build(), + "test('/dash')" + ), + Arguments.of( + EnumPropertySpec.builder("test") + .parameter("%L", "10") + .build(), + "test(10)" + ), + Arguments.of( + EnumPropertySpec.builder("dashboard") + .parameter("%C", "Dashboard") + .parameter("%C", "/dashboard") + .parameter("%L", "false") + .build(), + "dashboard('Dashboard', '/dashboard', false)" + ), + ) + + @JvmStatic + private fun propertiesWithAnnotations() = Stream.of( + Arguments.of( + EnumPropertySpec.builder("test") + .annotations(AnnotationSpec.builder("jsonIgnore").build()).build(), + """ + @jsonIgnore + test + """.trimIndent() + ), + Arguments.of( + EnumPropertySpec.builder("test") + .annotations( + AnnotationSpec.builder("jsonIgnore").build(), + AnnotationSpec.builder("JsonKey") + .content("name: %C", "test").build() + ).build(), + """ + @jsonIgnore + @JsonKey(name: 'test') + test + """.trimIndent() + ) + ) + } + + @ParameterizedTest + @MethodSource("propertiesWithAnnotations") + fun `test property generation with annotations`(propertySpec: EnumPropertySpec, expected: String) { + assertEquals(expected, propertySpec.toString()) + } + + @ParameterizedTest + @MethodSource("properties") + fun `test property generation`(propertySpec: EnumPropertySpec, expected: String) { + assertEquals(expected, propertySpec.toString()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/ExtensionWriterTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/ExtensionWriterTest.kt new file mode 100644 index 00000000..df2ddfd8 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/ExtensionWriterTest.kt @@ -0,0 +1,115 @@ +package net.theevilreaper.dartpoet.code.writer + +import com.google.common.truth.Truth.assertThat +import net.theevilreaper.dartpoet.code.CodeBlock +import net.theevilreaper.dartpoet.extension.ExtensionSpec +import net.theevilreaper.dartpoet.function.FunctionSpec +import net.theevilreaper.dartpoet.type.ClassName +import net.theevilreaper.dartpoet.type.ParameterizedTypeName.Companion.parameterizedBy +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class ExtensionWriterTest { + + companion object { + + @JvmStatic + private fun basicExtensions() = Stream.of( + Arguments.of( + ExtensionSpec.builder("TestExtension", String::class).build(), + "extension TestExtension on String {}" + ), + Arguments.of( + ExtensionSpec.unnamed(String::class).build(), //Unnamed extensions + "extension on String {}" + ), + ) + + @JvmStatic + private fun comments() = Stream.of( + Arguments.of( + ExtensionSpec.builder("StringExt", String::class) + .doc("This is a first line of documentation") + .build(), + """ + /// This is a first line of documentation + extension StringExt on String {} + """.trimIndent() + ), + Arguments.of( + ExtensionSpec.builder("StringExt", String::class) + .doc("This is a first line of documentation") + .doc("Second line of comment") + .build(), + """ + /// This is a first line of documentation + /// Second line of comment + extension StringExt on String {} + """.trimIndent() + ), + ) + + @JvmStatic + private fun basicGenericExtension() = Stream.of( + Arguments.of( + ExtensionSpec.builder("ListExt", List::class.parameterizedBy(ClassName("T"))) + .genericTypes(ClassName("T")) + .build(), + "extension ListExt on List {}" + ), + Arguments.of( + ExtensionSpec.builder("MapExt", Map::class.parameterizedBy(ClassName("T"), ClassName("E"))) + .genericTypes(ClassName("T"), ClassName("E")) + .build(), + "extension MapExt on Map {}" + ), + ) + } + + @ParameterizedTest + @MethodSource("basicExtensions") + fun `test basic extension`(extensionSpec: ExtensionSpec, expected: String) { + assertThat(extensionSpec.toString()).isEqualTo(expected) + } + + @ParameterizedTest + @MethodSource("comments") + fun `test comments on extension classes`(extensionSpec: ExtensionSpec, expected: String) { + assertThat(extensionSpec.toString()).isEqualTo(expected) + } + + @ParameterizedTest + @MethodSource("basicGenericExtension") + fun `test generic extension`(extensionSpec: ExtensionSpec, expected: String) { + assertThat(extensionSpec.toString()).isEqualTo(expected) + } + + @Test + fun `test simple extension with method`() { + val extension = ExtensionSpec.builder("TestExtension", String::class) + .function { + FunctionSpec.builder("hasSize") + .returns(Boolean::class) + .addCode( + CodeBlock.of( + "return this.length > 2;" + ) + ) + .build() + } + .build() + + assertThat(extension.toString()).isEqualTo( + """ + extension TestExtension on String { + bool hasSize() { + return this.length > 2; + } + } + """.trimIndent() + ) + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/FunctionWriterTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/FunctionWriterTest.kt new file mode 100644 index 00000000..bf4336a9 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/FunctionWriterTest.kt @@ -0,0 +1,249 @@ +package net.theevilreaper.dartpoet.code.writer + +import com.google.common.truth.Truth.assertThat +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.code.CodeBlock +import net.theevilreaper.dartpoet.code.CodeWriter +import net.theevilreaper.dartpoet.code.buildCodeBlock +import net.theevilreaper.dartpoet.function.FunctionSpec +import net.theevilreaper.dartpoet.parameter.ParameterSpec +import net.theevilreaper.dartpoet.type.ClassName +import net.theevilreaper.dartpoet.type.DYNAMIC +import net.theevilreaper.dartpoet.type.ParameterizedTypeName.Companion.parameterizedBy +import net.theevilreaper.dartpoet.type.asClassName +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class FunctionWriterTest { + + private companion object { + + @JvmStatic + private fun castFunctionWrite() = Stream.of( + Arguments.of( + FunctionSpec.builder("getId").returns(Int::class).typeCast(Int::class).build(), + "int getId();" + ), + Arguments.of( + FunctionSpec.builder("getModels").returns(List::class.parameterizedBy(ClassName("Model"))) + .typeCast(List::class.parameterizedBy(DYNAMIC)) + .build(), + "List getModels>();", + ) + ) + + @JvmStatic + private fun basicFunctionWrites(): Stream = Stream.of( + Arguments.of( + FunctionSpec.builder("test") + .returns(Void::class) + .build(), + "void test();" + ), + Arguments.of( + FunctionSpec.builder("getAllById") + .returns(List::class.parameterizedBy(ClassName("Model"))) + .parameters( + ParameterSpec.builder("id", String::class).build(), + ParameterSpec.builder("amount", Int::class).build() + ) + .build(), + "List getAllById(String id, int amount);" + ), + Arguments.of( + FunctionSpec.builder("test") + .returns(Void::class) + .parameters( + ParameterSpec.builder("id", String::class).nullable(true).build(), + ParameterSpec.builder("amount", Int::class).build() + ) + .build(), + "void test(String? id, int amount);" + ), + ) + } + + @ParameterizedTest + @MethodSource("castFunctionWrite") + fun `test function write with cast typeNames`(functionSpec: FunctionSpec, expected: String) { + assertThat(functionSpec.toString()).isEqualTo(expected) + } + + @ParameterizedTest + @MethodSource("basicFunctionWrites") + fun `test basic function write`(functionSpec: FunctionSpec, expected: String) { + assertThat(functionSpec.toString()).isEqualTo(expected) + } + + @Test + fun `write simple method without parameters`() { + val writer = CodeWriter(StringBuilder()) + val method = FunctionSpec.builder("getName") + .modifier(DartModifier.PUBLIC) + .returns(String::class) + .addCode("return %C;", "test") + .build() + writer.close() + + assertThat(method.toString()).isEqualTo( + """ + String getName() { + return 'test'; + } + """.trimIndent() + ) + } + + @Test + fun `test simple private method`() { + val writer = CodeWriter(StringBuilder()) + val method = FunctionSpec.builder("name") + .returns(String::class) + .modifier(DartModifier.PRIVATE) + .addCode("return %C;", "Tobi").build() + writer.close() + assertThat(method.toString()).isEqualTo( + """ + String _name() { + return 'Tobi'; + } + """.trimIndent() + ) + } + + @Test + fun `write simple nullable function`() { + val method = FunctionSpec.builder("getId") + .returns(Int::class.asClassName().copy(nullable = true)) + .addCode("return %L;", 10).build() + assertThat(method.toString()).isEqualTo( + """ + int? getId() { + return 10; + } + """.trimIndent() + ) + } + + @Test + fun `write another nullable method`() { + val method = FunctionSpec.builder("getValue") + .returns(Int::class.asClassName().copy(nullable = true)) + .addCode("return 1;") + .build() + assertThat(method.toString()).isEqualTo( + """ + int? getValue() { + return 1; + } + """.trimIndent() + ) + } + + @Test + fun `write simple async function`() { + val method = FunctionSpec.builder("getNameById") + .returns(String::class) + .async(true) + .parameter { + ParameterSpec.builder("id", Int::class).build() + } + .addCode( + CodeBlock.builder() + .add("return 'Thomas';") + .build() + ) + .build() + assertThat(method.toString()).isEqualTo( + """ + Future getNameById(int id) async { + return 'Thomas'; + } + """.trimIndent() + ) + } + + @Test + fun `test other getter variant write`() { + val function = FunctionSpec.builder("value") + .returns(Int::class) + .getter(true) + .addCode("%L", "_value;") + .build() + assertThat(function.toString()).isEqualTo("int get value => _value;"); + } + + @Test + fun `test other setter variant write`() { + val function = FunctionSpec.builder("value") + .parameter( + ParameterSpec.builder("value", Int::class).build() + ) + .setter(true) + .addCode(buildCodeBlock { + add("%L = %L;", "_value", "value") + }) + .build() + assertThat(function.toString()).isEqualTo( + """ + set value(int value) { + _value = value; + } + """.trimIndent() + ) + } + + @Test + fun `test lambda method write`() { + val function = FunctionSpec.builder("isNoble") + .lambda(true) + .parameter(ParameterSpec.builder("atomicNumber", Int::class).build()) + .returns(Boolean::class) + .addCode("_nobleGases[atomicNumber] != null;") + .build() + assertThat(function.toString()).isEqualTo("bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;") + } + + @Test + fun `test method with documentation`() { + val function = FunctionSpec.builder("getName") + .returns(String::class) + .addCode("return %C;", "Test") + .doc("Returns the name from an object") + .doc("For generation tests it returns 'Test'") + .build() + assertThat(function.toString()).isEqualTo( + """ + /// Returns the name from an object + /// For generation tests it returns 'Test' + String getName() { + return 'Test'; + } + """.trimIndent() + ) + } + + @Test + fun `test function write with named and required parameters`() { + val functionSpec = FunctionSpec.builder("testMethod") + .modifiers(DartModifier.ABSTRACT) + .parameters( + ParameterSpec.builder("a", String::class).named(true).nullable(true).build(), + ParameterSpec.builder("b", String::class).named(true).required().build(), + ParameterSpec.builder("c", Int::class) + .named(true) + .required() + .initializer("%L", "10") + .build() + ) + .build() + assertThat(functionSpec.toString()).isEqualTo( + """ + abstract void testMethod({required String b, String? a, int c = 10}); + """.trimIndent() + ) + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/ParameterWriterTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/ParameterWriterTest.kt new file mode 100644 index 00000000..81019f94 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/ParameterWriterTest.kt @@ -0,0 +1,56 @@ +package net.theevilreaper.dartpoet.code.writer + +import com.google.common.truth.Truth.assertThat +import net.theevilreaper.dartpoet.parameter.ParameterSpec +import net.theevilreaper.dartpoet.type.ParameterizedTypeName.Companion.parameterizedBy +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class ParameterWriterTest { + + companion object { + + @JvmStatic + private fun parameterTests(): Stream = Stream.of( + Arguments.of(ParameterSpec.builder("age", Int::class).build(), "int age"), + Arguments.of(ParameterSpec.builder("age", Int::class).initializer("%L", "10").build(), "int age = 10"), + Arguments.of(ParameterSpec.builder("test", String::class).nullable(true).build(), "String? test"), + Arguments.of( + ParameterSpec.builder("list", List::class.parameterizedBy(Int::class)).build(), + "List list" + ), + Arguments.of( + ParameterSpec.builder("list", List::class.parameterizedBy(Int::class)).initializer("%L", "[]").build(), + "List list = []" + ), + Arguments.of( + ParameterSpec.builder("map", Map::class.parameterizedBy(String::class, Int::class)).build(), + "Map map" + ), + Arguments.of( + ParameterSpec.builder("map", Map::class.parameterizedBy(String::class, Int::class)) + .initializer("%L", "{}").build(), + "Map map = {}" + ) + ) + } + + @ParameterizedTest + @MethodSource("parameterTests") + fun `test parameter write`(parameterSpec: ParameterSpec, expected: String) { + assertThat(parameterSpec.toString()).isEqualTo(expected) + } + + @Test + fun `test invalid parameter definition`() { + assertThrows( + IllegalStateException::class.java, + { ParameterSpec.builder("").build() }, + "The name of a parameter can't be empty" + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/PropertyWriterTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/PropertyWriterTest.kt new file mode 100644 index 00000000..21dfb568 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/PropertyWriterTest.kt @@ -0,0 +1,115 @@ +package net.theevilreaper.dartpoet.code.writer + +import com.google.common.truth.Truth.assertThat +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import net.theevilreaper.dartpoet.property.PropertySpec +import net.theevilreaper.dartpoet.type.asTypeName +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream +import kotlin.test.assertEquals + +class PropertyWriterTest { + + companion object { + + @JvmStatic + private fun simpleProperties(): Stream = Stream.of( + Arguments.of(PropertySpec.builder("id", Int::class).build(), "int id;"), + Arguments.of( + PropertySpec.builder("id", String::class.asTypeName().copy(true)) + .build(), + "String? id;" + ), + Arguments.of( + PropertySpec.builder("test", String::class.asTypeName().copy(nullable = true)) + .modifier { DartModifier.PRIVATE } + .build(), + "String? _test;" + ), + Arguments.of( + PropertySpec.builder("abc", String::class) + .modifier { DartModifier.LATE } + .build(), + "late String abc;" + ), + Arguments.of( + PropertySpec.builder("age", Int::class) + .initWith("%L", "12") + .build(), + "int age = 12;" + ) + ) + + @JvmStatic + private fun constPropertyWrite() = Stream.of( + Arguments.of( + PropertySpec.constBuilder("value").initWith("%L", "12").build(), + "static const value = 12;" + ), + Arguments.of( + PropertySpec.constBuilder("test", String::class).initWith("%C", "Test").build(), + "static const String test = 'Test';" + ) + ) + } + + @ParameterizedTest + @MethodSource("simpleProperties") + fun `test simple properties`(propertySpec: PropertySpec, expected: String) { + assertEquals(expected, propertySpec.toString()) + } + + @ParameterizedTest + @MethodSource("constPropertyWrite") + fun `test const property writing`(propertySpec: PropertySpec, expected: String) { + assertEquals(expected, propertySpec.toString()) + } + + @Test + fun `write simple variable with one annotation`() { + val property = PropertySpec.builder("age", Int::class) + .annotation { AnnotationSpec.builder("jsonIgnore").build() } + .initWith("%L", "12") + .build() + assertThat(property.toString()).isEqualTo( + """ + @jsonIgnore + int age = 12; + """.trimIndent() + ) + } + + @Test + fun `write property with annotations`() { + val property = PropertySpec.builder("description", String::class.asTypeName().copy(nullable = true)) + .annotation { + AnnotationSpec.builder("JsonKey") + .content("name: %C", "description") + .build() + } + .build() + assertThat(property.toString()).isEqualTo( + """ + @JsonKey(name: 'description') + String? description; + """.trimIndent() + ) + } + + @Test + fun `test property with comment`() { + val property = PropertySpec.builder("name", String::class.asTypeName().copy(nullable = true)) + .docs("Represents the name from something") + .build() + assertThat(property.toString()).isEqualTo( + """ + /// Represents the name from something + String? name; + """.trimIndent() + ) + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/TypeDefWriterTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/TypeDefWriterTest.kt new file mode 100644 index 00000000..3668c08f --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/code/writer/TypeDefWriterTest.kt @@ -0,0 +1,161 @@ +package net.theevilreaper.dartpoet.code.writer + +import com.google.common.truth.Truth +import net.theevilreaper.dartpoet.function.typedef.TypeDefSpec +import net.theevilreaper.dartpoet.parameter.ParameterSpec +import net.theevilreaper.dartpoet.type.ClassName +import net.theevilreaper.dartpoet.type.DYNAMIC +import net.theevilreaper.dartpoet.type.ParameterizedTypeName.Companion.parameterizedBy +import net.theevilreaper.dartpoet.type.asTypeName +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class TypeDefWriterTest { + + companion object { + + private val genericClassName = ClassName("E") + private val secondGenericClassName = ClassName("T") + + @JvmStatic + private fun typeDefs(): Stream = Stream.of( + Arguments.of( + TypeDefSpec.builder("ValueUpdate", genericClassName) + .parameter( + ParameterSpec.builder("value", genericClassName) + .nullable(true) + .build() + ) + .name("Function") + .build(), + "typedef ValueUpdate = void Function(E? value);" + ), + Arguments.of( + TypeDefSpec.builder("json") + .returns(Map::class.parameterizedBy(String::class.asTypeName(), DYNAMIC)) + .build(), + "typedef json = Map;" + ), + ) + + @JvmStatic + private fun multipleCastArguments(): Stream = Stream.of( + Arguments.of( + TypeDefSpec.builder( + "DoubleValueUpdate", + genericClassName, secondGenericClassName + ) + .name("Function") + .parameters( + ParameterSpec.builder("first", genericClassName) + .nullable(true) + .build(), + ParameterSpec.builder("second", secondGenericClassName) + .nullable(true) + .build() + ) + .build(), + "typedef DoubleValueUpdate = void Function(E? first, T? second);" + ), + Arguments.of( + TypeDefSpec.builder("Compare", genericClassName, secondGenericClassName) + .returns(Int::class) + .name("Function") + .parameters( + ParameterSpec.builder("a", genericClassName) + .build(), + ParameterSpec.builder("b", genericClassName) + .build() + ) + .build(), + "typedef Compare = int Function(E a, E b);" + ) + ) + + @JvmStatic + private fun differentParameterTypes(): Stream = Stream.of( + Arguments.of( + TypeDefSpec.builder("ValueUpdate", genericClassName) + .name("Function") + .returns(genericClassName) + .parameters( + ParameterSpec.builder("value", String::class) + .build(), + ParameterSpec.builder("data", genericClassName) + .nullable(true) + .initializer("%L", "null") + .build() + ) + .build(), + "typedef ValueUpdate = E Function(String value, [E? data = null]);" + ), + Arguments.of( + TypeDefSpec.builder("ValueUpdate", genericClassName) + .name("Function") + .returns(genericClassName) + .parameters( + ParameterSpec.builder("map", Map::class.parameterizedBy(String::class, Int::class)) + .build(), + ParameterSpec.builder("data", genericClassName) + .nullable(true) + .initializer("%L", "null") + .build() + ) + .build(), + "typedef ValueUpdate = E Function(Map map, [E? data = null]);" + ), + Arguments.of( + TypeDefSpec.builder("ValueUpdate", genericClassName) + .name("Function") + .returns(genericClassName) + .parameters( + ParameterSpec.builder("list", List::class.parameterizedBy(String::class)) + .build(), + ParameterSpec.builder("data", genericClassName) + .required() + .build(), + ) + .build(), + "typedef ValueUpdate = E Function(List list, {required E data});" + ), + Arguments.of( + TypeDefSpec.builder("ValueUpdate", genericClassName) + .name("Function") + .returns(genericClassName) + .parameters( + ParameterSpec.builder("data", genericClassName) + .build(), + ParameterSpec.builder("a", String::class).named(true).nullable(true).build(), + ParameterSpec.builder("b", String::class).named(true).required().build(), + ParameterSpec.builder("c", Int::class) + .named(true) + .required() + .initializer("%L", "10") + .build() + ) + .build(), + "typedef ValueUpdate = E Function(E data, {required String b, String? a, int c = 10});" + ) + ) + } + + @ParameterizedTest + @MethodSource("typeDefs") + fun `test typedef write`(typeDef: TypeDefSpec, expected: String) { + Truth.assertThat(typeDef.toString()).isEqualTo(expected) + } + + @ParameterizedTest + @MethodSource("multipleCastArguments") + fun `test typedef write with multiple casts`(typeDef: TypeDefSpec, expected: String) { + Truth.assertThat(typeDef.toString()).isEqualTo(expected) + } + + @ParameterizedTest + @MethodSource("differentParameterTypes") + fun `test typedef write with different parameter types`(typeDef: TypeDefSpec, expected: String) { + Truth.assertThat(typeDef.toString()).isEqualTo(expected) + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/directive/DirectiveFactoryTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/directive/DirectiveFactoryTest.kt new file mode 100644 index 00000000..806589eb --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/directive/DirectiveFactoryTest.kt @@ -0,0 +1,34 @@ +package net.theevilreaper.dartpoet.directive + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class DirectiveFactoryTest { + + companion object { + + @JvmStatic + private fun invalidFactoryUsage() = Stream.of( + Arguments.of( + { DirectiveFactory.create(DirectiveType.LIBRARY, "") }, + "The library directive doesn't support a cast type or import cast. Please use #createLibDirective method instead" + ), + Arguments.of( + { DirectiveFactory.create(DirectiveType.LIBRARY, "", castType = CastType.HIDE) }, + "The library directive doesn't support a cast type or import cast. Please use #createLibDirective method instead" + ), + ) + } + + @ParameterizedTest + @MethodSource("invalidFactoryUsage") + fun `test invalid factory usage`(current: () -> Directive, expectedMessage: String) { + val exception = assertThrows { current() } + assertEquals(IllegalStateException::class.java, exception.javaClass) + assertEquals(expectedMessage, exception.message) + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/directive/DirectiveSortTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/directive/DirectiveSortTest.kt new file mode 100644 index 00000000..3bcc66a6 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/directive/DirectiveSortTest.kt @@ -0,0 +1,88 @@ +package net.theevilreaper.dartpoet.directive + +import net.theevilreaper.dartpoet.util.DirectiveOrdering +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +class DirectiveSortTest { + + companion object { + + @JvmStatic + private fun dartDirectiveArguments(): Stream = Stream.of( + Arguments.of( + listOf( + DirectiveFactory.create(DirectiveType.IMPORT, "dart:io"), + DirectiveFactory.create(DirectiveType.IMPORT, "dart:math"), + DirectiveFactory.create(DirectiveType.PART, "test.dart"), + ), + listOf( + "dart:io", + "dart:math" + ) + ) + ) + + @JvmStatic + private fun directiveSortArguments(): Stream = Stream.of( + Arguments.of( + listOf( + DirectiveFactory.create(DirectiveType.IMPORT, "testD.dart"), + DirectiveFactory.create(DirectiveType.IMPORT, "testA.dart"), + DirectiveFactory.create(DirectiveType.IMPORT, "testB.dart"), + DirectiveFactory.create(DirectiveType.IMPORT, "testC.dart"), + ), + listOf( + "testA.dart", + "testB.dart", + "testC.dart", + "testD.dart", + ) + ) + ) + } + + @ParameterizedTest + @MethodSource("dartDirectiveArguments") + fun `test dart directive sort`(directives: List, expected: List) { + val sortedData = DirectiveOrdering.sortDirectives(DartDirective::class, directives) + assertNotEquals(sortedData.size, directives.size) + val trimmedData = formatImports(sortedData, "import '", "';") + assertEquals(expected.size, sortedData.size) + assertEquals(expected, trimmedData) + } + + @ParameterizedTest + @MethodSource("directiveSortArguments") + fun `test package directive sort`(directives: List, expected: List) { + val sortedData = DirectiveOrdering.sortDirectives(directives) + val trimmedData = formatImports(sortedData, "import 'package:", "';") + assertEquals(expected.size, sortedData.size) + assertEquals(expected, trimmedData) + } + + @Test + fun `test empty ordering result`() { + val sortedData = DirectiveOrdering.sortDirectives(listOf()) + assertEquals(emptyList(), sortedData) + assertEquals(emptyList(), DirectiveOrdering.sortDirectives(DartDirective::class, listOf())) + } + + private inline fun formatImports( + directives: List, + vararg placeholders: String, + ): List { + return directives.map { + var directive: String = it.asString() + for (placeholder in placeholders) { + directive = directive.replace(placeholder, "") + } + return@map directive + }.toList() + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/directive/DirectiveTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/directive/DirectiveTest.kt new file mode 100644 index 00000000..a788ad1c --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/directive/DirectiveTest.kt @@ -0,0 +1,155 @@ +package net.theevilreaper.dartpoet.directive + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class DirectiveTest { + + private val packageImport = "import 'package:flutter/material.dart';" + + companion object { + private const val CAST_VALUE = "item" + private const val TEST_IMPORT = "../../model/item_model.dart" + private const val EMPTY_NAME_MESSAGE = "The path of an directive can't be empty" + private const val INVALID_CAST_USAGE = + "The castType and importCast must be set together or must be null. A mixed state is not allowed" + + @JvmStatic + private fun directivesWhichThrowsException() = Stream.of( + Arguments.of({ DartDirective(" ") }, EMPTY_NAME_MESSAGE), + Arguments.of({ LibraryDirective("") }, EMPTY_NAME_MESSAGE), + Arguments.of({ PartDirective("") }, EMPTY_NAME_MESSAGE), + Arguments.of({ RelativeDirective("") }, EMPTY_NAME_MESSAGE), + Arguments.of({ LibraryDirective("") }, EMPTY_NAME_MESSAGE), + Arguments.of({ DartDirective("flutter/material.dart", CastType.AS, null) }, INVALID_CAST_USAGE), + Arguments.of({ DartDirective("flutter/material.dart", null, "test") }, INVALID_CAST_USAGE), + Arguments.of({ RelativeDirective("flutter/material.dart", CastType.AS, null) }, INVALID_CAST_USAGE), + Arguments.of({ RelativeDirective("flutter/material.dart", null, "test") }, INVALID_CAST_USAGE), + Arguments.of({ ExportDirective("test.dart", CastType.HIDE, null) }, INVALID_CAST_USAGE), + Arguments.of({ ExportDirective("test.dart", null, "test") }, INVALID_CAST_USAGE) + ) + + @JvmStatic + private fun libDirectives() = Stream.of( + Arguments.of( + DirectiveFactory.createLib("testLib"), + "library testLib;" + ), + Arguments.of( + DirectiveFactory.createLib("testLib", true), + "part of testLib;" + ), + ) + + @JvmStatic + private fun dartDirectives() = Stream.of( + Arguments.of(DirectiveFactory.create(DirectiveType.IMPORT, "dart:html"), "import 'dart:html';"), + Arguments.of( + DirectiveFactory.create(DirectiveType.IMPORT, "dart:http", CastType.AS, "http"), + "import 'dart:http' as http;" + ) + ) + + @JvmStatic + private fun relativeDirectives() = Stream.of( + Arguments.of(RelativeDirective(TEST_IMPORT), "import '../../model/item_model.dart';"), + Arguments.of( + RelativeDirective(TEST_IMPORT, CastType.AS, CAST_VALUE), + "import '../../model/item_model.dart' as item;" + ), + Arguments.of( + RelativeDirective(TEST_IMPORT, CastType.DEFERRED, CAST_VALUE), + "import '../../model/item_model.dart' deferred as item;" + ), + Arguments.of( + RelativeDirective(TEST_IMPORT, CastType.HIDE, CAST_VALUE), + "import '../../model/item_model.dart' hide item;" + ), + Arguments.of( + DirectiveFactory.create(DirectiveType.RELATIVE, TEST_IMPORT), + "import '../../model/item_model.dart';" + ), + Arguments.of( + DirectiveFactory.create(DirectiveType.RELATIVE, TEST_IMPORT, CastType.AS, CAST_VALUE), + "import '../../model/item_model.dart' as item;" + ), + Arguments.of( + DirectiveFactory.create(DirectiveType.RELATIVE, TEST_IMPORT, CastType.DEFERRED, CAST_VALUE), + "import '../../model/item_model.dart' deferred as item;" + ), + Arguments.of( + DirectiveFactory.create(DirectiveType.RELATIVE, TEST_IMPORT, CastType.HIDE, CAST_VALUE), + "import '../../model/item_model.dart' hide item;" + ), + Arguments.of( + DirectiveFactory.create(DirectiveType.RELATIVE, TEST_IMPORT, CastType.SHOW, CAST_VALUE), + "import '../../model/item_model.dart' show item;" + ) + ) + + @JvmStatic + private fun exportDirectives() = Stream.of( + Arguments.of(DirectiveFactory.create(DirectiveType.EXPORT, "test.dart"), "export 'test.dart';"), + Arguments.of(DirectiveFactory.create(DirectiveType.EXPORT, "new_lib"), "export 'new_lib.dart';") + ) + } + + @ParameterizedTest + @MethodSource("directivesWhichThrowsException") + fun `test directives which throws exception`(current: () -> Directive, expectedMessage: String) { + val exception = assertThrows { current() } + assertEquals(IllegalStateException::class.java, exception.javaClass) + assertEquals(expectedMessage, exception.message) + } + + @ParameterizedTest + @MethodSource("libDirectives") + fun `test library imports`(current: Directive, expected: String) { + assertEquals(expected, current.asString()) + } + + @ParameterizedTest + @MethodSource("dartDirectives") + fun `test dart imports`(current: Directive, expected: String) { + assertEquals(expected, current.asString()) + } + + @ParameterizedTest + @MethodSource("relativeDirectives") + fun `test relative dart imports`(current: Directive, expected: String) { + assertEquals(expected, current.asString()) + } + + @ParameterizedTest + @MethodSource("exportDirectives") + fun `test export directive`(current: Directive, expected: String) { + assertEquals(expected, current.asString()) + } + + @Test + fun `test cast import with empty cast`() { + assertThrows( + IllegalStateException::class.java, + { DirectiveFactory.create(DirectiveType.IMPORT, "flutter/material.dart", CastType.AS, " ") }, + "The importCast can't be empty" + ) + assertThrows( + IllegalStateException::class.java, + { DirectiveFactory.create(DirectiveType.IMPORT, "flutter/material.dart", CastType.AS, "") }, + "The importCast can't be empty" + ) + } + + + @Test + fun `test package import`() { + val import = DirectiveFactory.create(DirectiveType.IMPORT, "flutter/material.dart") + assertEquals(packageImport, import.asString()) + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/directive/ExportDirectiveTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/directive/ExportDirectiveTest.kt new file mode 100644 index 00000000..cfa91fb1 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/directive/ExportDirectiveTest.kt @@ -0,0 +1,58 @@ +package net.theevilreaper.dartpoet.directive + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class ExportDirectiveTest { + + companion object { + + @JvmStatic + private fun invalidExportDirectives(): Stream = Stream.of( + Arguments.of( + { DirectiveFactory.create(DirectiveType.EXPORT, "") }, + "The path of an directive can't be empty" + ), + Arguments.of( + { DirectiveFactory.create(DirectiveType.EXPORT, "dart:math", CastType.DEFERRED, "math") }, + "The following cast types are not allowed for an export directive: [DEFERRED, AS]" + ), + Arguments.of( + { DirectiveFactory.create(DirectiveType.EXPORT, "dart:math", CastType.AS, "math") }, + "The following cast types are not allowed for an export directive: [DEFERRED, AS]" + ) + ) + + @JvmStatic + private fun exportDirectives(): Stream = Stream.of( + Arguments.of(DirectiveFactory.create(DirectiveType.EXPORT, "test.dart"), "export 'test.dart';"), + Arguments.of(DirectiveFactory.create(DirectiveType.EXPORT, "new_lib"), "export 'new_lib.dart';"), + Arguments.of( + DirectiveFactory.create(DirectiveType.EXPORT, "new_lib", CastType.SHOW, "lib"), + "export 'new_lib.dart' show lib;" + ), + Arguments.of( + DirectiveFactory.create(DirectiveType.EXPORT, "new_lib", CastType.HIDE, "lib"), + "export 'new_lib.dart' hide lib;" + ) + ) + } + + @ParameterizedTest + @MethodSource("invalidExportDirectives") + fun `test invalid export directives`(current: () -> ExportDirective, expected: String) { + val exception = assertThrows { current() } + assertEquals(IllegalStateException::class.java, exception.javaClass) + assertEquals(expected, exception.message) + } + + @ParameterizedTest + @MethodSource("exportDirectives") + fun `test export directives`(current: ExportDirective, expected: String) { + assertEquals(expected, current.asString()) + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/directive/PartDirectiveTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/directive/PartDirectiveTest.kt new file mode 100644 index 00000000..f202233e --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/directive/PartDirectiveTest.kt @@ -0,0 +1,30 @@ +package net.theevilreaper.dartpoet.directive + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class PartDirectiveTest { + + private val expectedImport = "part 'item_model.freezed.dart';" + + @Test + fun `test import with empty path`() { + Assertions.assertThrows( + IllegalStateException::class.java, + { DirectiveFactory.create(DirectiveType.IMPORT, " ") }, + "The path of an Import can't be empty" + ) + Assertions.assertThrows( + IllegalStateException::class.java, + { DirectiveFactory.create(DirectiveType.RELATIVE, " ") }, + "The path of an Import can't be empty" + ) + } + + @Test + fun `create part import`() { + val partImport = DirectiveFactory.create(DirectiveType.PART, "item_model.freezed.dart") + assertEquals(expectedImport, partImport.asString()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/enumeration/EnumPropertySpecTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/enumeration/EnumPropertySpecTest.kt new file mode 100644 index 00000000..6c3035de --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/enumeration/EnumPropertySpecTest.kt @@ -0,0 +1,67 @@ +package net.theevilreaper.dartpoet.enumeration + +import com.google.common.truth.Truth +import net.theevilreaper.dartpoet.enum.EnumPropertySpec +import net.theevilreaper.dartpoet.type.ClassName +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class EnumPropertySpecTest { + + companion object { + + @JvmStatic + private fun values() = Stream.of( + Arguments.of(EnumPropertySpec.builder("test").build(), "test"), + Arguments.of(EnumPropertySpec.builder("test").generic(String::class).build(), "test"), + Arguments.of( + EnumPropertySpec.builder("navigation") + .parameter("%C", "/dashboard") + .build(), + "navigation('/dashboard')" + ) + ) + + @JvmStatic + private fun genericEnumProperties(): Stream = Stream.of( + Arguments.of(EnumPropertySpec.builder("test").generic(String::class).build(), "test"), + Arguments.of(EnumPropertySpec.builder("test").generic(ClassName("E")).build(), "test"), + ) + } + + @ParameterizedTest + @MethodSource("values") + fun `test enum property generation`(spec: EnumPropertySpec, excepted: String) { + Truth.assertThat(spec.toString()).isEqualTo(excepted) + } + + @ParameterizedTest + @MethodSource("genericEnumProperties") + fun `test generic enum property generation`(spec: EnumPropertySpec, excepted: String) { + Truth.assertThat(spec.toString()).isEqualTo(excepted) + } + + @Test + fun `test spec conversion to a builder`() { + val propertySpec = EnumPropertySpec + .builder("test") + .generic(String::class) + .parameter("%C", "/dashboard") + .build() + val specAsBuilder = propertySpec.toBuilder() + assertNotNull(specAsBuilder) + assertEquals(propertySpec.name, specAsBuilder.name) + assertEquals(propertySpec.generic, specAsBuilder.genericValueCast) + assertTrue { propertySpec.annotations.isEmpty() } + assertTrue { specAsBuilder.annotations.isEmpty() } + assertTrue { specAsBuilder.parameters.isNotEmpty() } + assertContentEquals(propertySpec.parameters, specAsBuilder.parameters) + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/extension/ExtensionSpecTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/extension/ExtensionSpecTest.kt new file mode 100644 index 00000000..f9d52285 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/extension/ExtensionSpecTest.kt @@ -0,0 +1,74 @@ +package net.theevilreaper.dartpoet.extension + +import net.theevilreaper.dartpoet.type.ClassName +import net.theevilreaper.dartpoet.type.ParameterizedTypeName.Companion.parameterizedBy +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class ExtensionSpecTest { + + companion object { + + @JvmStatic + private fun invalidExtensionSpecs() = Stream.of( + Arguments.of( + IllegalStateException::class.java, + { ExtensionSpec.builder("", String::class).build() }, + "The name of a extension can't be empty" + ), + Arguments.of( + IllegalArgumentException::class.java, + { ExtensionSpec.builder("StringExt", "").build() }, + "The name of a ClassName can't be empty (includes only spaces)" + ), + Arguments.of( + IllegalStateException::class.java, + { + ExtensionSpec.builder("ListExt", List::class.parameterizedBy(String::class)) + .genericTypes(Int::class) + .build() + }, + """ + The generic usage from the genericCast and extensionClass is not the same. + Expected 'int' but got in the extension class: 'String' + """.trimIndent() + ), + Arguments.of( + IllegalStateException::class.java, + { + ExtensionSpec.builder("MapExt", Map::class.parameterizedBy(String::class, Int::class)) + .genericTypes(ClassName("D"), ClassName("D")) + .build() + }, + """ + The generic usage from the genericCast and extensionClass is not the same. + Expected 'D, D' but got in the extension class: 'String, int' + """.trimIndent() + ) + ) + } + + @ParameterizedTest + @MethodSource("invalidExtensionSpecs") + fun `test invalid extension spec`(exception: Class, function: () -> Unit, message: String) { + val givenException = assertThrows(exception) { function() } + assertEquals(message, givenException.message) + } + + @Test + fun `test spec to builder conversation`() { + val extensionSpec = ExtensionSpec.builder("isEmpty", "String") + .endsWithNewLine(true) + .doc("%C", "This is a test line") + .build() + val specAsBuilder = extensionSpec.toBuilder() + assertEquals(extensionSpec.name, specAsBuilder.name) + assertEquals(extensionSpec.extClass, specAsBuilder.extClass) + assertTrue { specAsBuilder.docs.isNotEmpty() } + assertEquals(extensionSpec.endWithNewLine, specAsBuilder.endWithNewLine) + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/function/FunctionSpecTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/function/FunctionSpecTest.kt new file mode 100644 index 00000000..b300afa3 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/function/FunctionSpecTest.kt @@ -0,0 +1,164 @@ +package net.theevilreaper.dartpoet.function + +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.parameter.ParameterSpec +import net.theevilreaper.dartpoet.type.asTypeName +import net.theevilreaper.dartpoet.util.NO_PARAMETER_TYPE +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.junit.jupiter.params.shadow.com.univocity.parsers.common.ArgumentUtils +import java.util.stream.Stream +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class FunctionSpecTest { + + companion object { + + @JvmStatic + private fun invalidFunctionTests(): Stream = Stream.of( + Arguments.of( + { FunctionSpec.builder(" ").build() }, + IllegalArgumentException::class.java, + "The name of a function can't be empty" + ), + Arguments.of( + { FunctionSpec.builder("getName").modifier(DartModifier.ABSTRACT).addCode("%L", "value").build() }, + IllegalArgumentException::class.java, + "An abstract method can't have a body" + ), + Arguments.of( + { + FunctionSpec.builder("print") + .modifiers(DartModifier.ABSTRACT) + .parameters( + ParameterSpec.builder("name", String::class).initializer("%C", "theEvilReaper").build(), + ParameterSpec.builder("value", Int::class).modifiers(DartModifier.REQUIRED).build() + ) + .build() + }, + IllegalArgumentException::class.java, + "A function can't have required and optional parameters" + ) + ) + + @JvmStatic + private fun testParameterGeneration(): Stream = Stream.of( + Arguments.of( + { + FunctionSpec.builder("print") + .modifiers(DartModifier.ABSTRACT) + .parameters( + ParameterSpec.builder("text", String::class).initializer("%C", "Hello World").build() + ) + .build() + }, + "abstract void print([String text = 'Hello World']);" + ), + Arguments.of( + { + FunctionSpec.builder("print") + .modifiers(DartModifier.ABSTRACT) + .parameters( + ParameterSpec.builder("text", String::class).required().build(), + ParameterSpec.builder("name", String::class).required().build() + ) + .build() + }, + "abstract void print({required String text, required String name});" + ), + Arguments.of( + { + FunctionSpec.builder("print") + .modifiers(DartModifier.ABSTRACT) + .parameters( + ParameterSpec.builder("text", String::class).required().build(), + ParameterSpec.builder("count", Int::class).build(), + ParameterSpec.builder("name", String::class).required().build(), + ) + .build() + }, + "abstract void print(int count, {required String text, required String name});" + ), + Arguments.of( + { + FunctionSpec.builder("print") + .modifiers(DartModifier.ABSTRACT) + .parameters( + ParameterSpec.builder("text", String::class).build(), + ParameterSpec.builder("additional", String::class).initializer("%C", "Hello World!") + .build(), + ) + .build() + }, + "abstract void print(String text, [String additional = 'Hello World!']);" + ), + ) + + @JvmStatic + private fun invalidFunctionParameters() = Stream.of( + Arguments.of( + { + FunctionSpec.builder("Test") + .parameter(ParameterSpec.builder("name").build()) + }, + NO_PARAMETER_TYPE + ), + Arguments.of( + { + FunctionSpec.builder("Test") + .parameter { ParameterSpec.builder("name").build() } + }, + NO_PARAMETER_TYPE + ), + Arguments.of( + { + FunctionSpec.builder("Test") + .parameters( + ParameterSpec.builder("test", String::class).build(), + ParameterSpec.builder("name").build() + ) + }, + NO_PARAMETER_TYPE + ) + ) + } + + @ParameterizedTest + @MethodSource("invalidFunctionTests") + fun `test invalid function`(specBuilder: () -> Unit, expected: Class, message: String) { + assertThrows(expected, specBuilder, message) + } + + @ParameterizedTest + @MethodSource("testParameterGeneration") + fun `test different parameter variants in combination`(specBuilder: () -> FunctionSpec, expected: String) { + val spec = specBuilder.invoke() + assertEquals(expected, spec.toString()) + } + + @ParameterizedTest + @MethodSource("invalidFunctionParameters") + fun `test invalid parameters on functions`(specBuilder: () -> Unit, expected: String) { + assertThrows(IllegalStateException::class.java, specBuilder, expected) + } + + @Test + fun `test spec to builder conversation`() { + val functionSpec = FunctionSpec.builder("getAmount") + .returns(Int::class.asTypeName().copy(nullable = true)) + .async(false) + .addCode("return %L", "10") + .build() + val specAsBuilder = functionSpec.toBuilder() + assertEquals(functionSpec.name, specAsBuilder.name) + assertEquals(functionSpec.returnType, specAsBuilder.returnType) + assertFalse { specAsBuilder.async } + assertTrue { specAsBuilder.returnType!!.isNullable } + assertTrue { specAsBuilder.body.isNotEmpty() } + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/function/constructor/ConstructorSpecTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/function/constructor/ConstructorSpecTest.kt new file mode 100644 index 00000000..1f59fe18 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/function/constructor/ConstructorSpecTest.kt @@ -0,0 +1,16 @@ +package net.theevilreaper.dartpoet.function.constructor + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class ConstructorSpecTest { + @Test + fun `test spec to builder conversation`() { + val constructorSpec = ConstructorSpec.builder("TestModel") + .asFactory(true) + .build() + val specAsBuilder = constructorSpec.toBuilder() + assertEquals(constructorSpec.name, specAsBuilder.name) + assertEquals(constructorSpec.isFactory, specAsBuilder.factory) + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/function/typedef/TypeDefSpecTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/function/typedef/TypeDefSpecTest.kt new file mode 100644 index 00000000..74483b32 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/function/typedef/TypeDefSpecTest.kt @@ -0,0 +1,53 @@ +package net.theevilreaper.dartpoet.function.typedef + +import net.theevilreaper.dartpoet.parameter.ParameterSpec +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class TypeDefSpecTest { + + companion object { + + @JvmStatic + private fun invalidTypeDefs(): Stream = Stream.of( + Arguments.of( + IllegalArgumentException::class.java, + { TypeDefSpec.builder("").build() }, + "The name of a typedef can't be empty" + ), + Arguments.of( + IllegalArgumentException::class.java, + { TypeDefSpec.builder("Test", Int::class) + .name("") + .returns(String::class).build() + }, + "The function name of a typedef can't be empty" + ) + ) + } + + @ParameterizedTest + @MethodSource("invalidTypeDefs") + fun `test invalid typedef creation`(exception: Class, function: () -> Unit, message: String) { + val exceptionMessage = assertThrows(exception, function, message).message + assertEquals(message, exceptionMessage) + } + + @Test + fun `test to builder method`() { + val typeSpec = TypeDefSpec.builder("Test", Int::class) + .name("Function") + .returns(String::class).build() + assertNotEquals(Void::class.java, typeSpec.returnType) + + val newBuilder = typeSpec.toBuilder() + newBuilder.parameter(ParameterSpec.builder("test", String::class).build()) + + val newTypeSpec = newBuilder.build() + assertNotEquals(typeSpec.parameters, newTypeSpec.parameters) + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/meta/SpecDataTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/meta/SpecDataTest.kt index 7b6a7af8..1bae7445 100644 --- a/src/test/kotlin/net/theevilreaper/dartpoet/meta/SpecDataTest.kt +++ b/src/test/kotlin/net/theevilreaper/dartpoet/meta/SpecDataTest.kt @@ -2,7 +2,7 @@ package net.theevilreaper.dartpoet.meta import net.theevilreaper.dartpoet.DartModifier import net.theevilreaper.dartpoet.annotation.AnnotationSpec -import org.junit.Test +import org.junit.jupiter.api.Test import kotlin.test.assertEquals class SpecDataTest { @@ -11,8 +11,8 @@ class SpecDataTest { @Test fun `test annotation add`() { - this.specData.annotation(AnnotationSpec()) - this.specData.annotation { AnnotationSpec() } + this.specData.annotation(AnnotationSpec.builder("jsonKey").build()) + this.specData.annotation { AnnotationSpec.builder("jsonIgnore").build() } assertEquals(2, this.specData.annotations.size) } diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/parameter/ParameterSpecTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/parameter/ParameterSpecTest.kt new file mode 100644 index 00000000..ed32231f --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/parameter/ParameterSpecTest.kt @@ -0,0 +1,59 @@ +package net.theevilreaper.dartpoet.parameter + +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.annotation.AnnotationSpec +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream +import kotlin.test.assertContentEquals + +class ParameterSpecTest { + + companion object { + + @JvmStatic + private fun parameterVariants(): Stream = Stream.of( + Arguments.of( + { ParameterSpec.builder("amount", Int::class).modifiers(DartModifier.REQUIRED) }, + "required int amount" + ), + Arguments.of( + { ParameterSpec.builder("name", String::class).initializer("%C", "theEvilReaper") }, + "String name = 'theEvilReaper'" + ), + Arguments.of( + { + ParameterSpec.builder("name", String::class).named(true).initializer("%C", "theEvilReaper") + }, + "String name = 'theEvilReaper'" + ) + ) + } + + @ParameterizedTest + @MethodSource("parameterVariants") + fun `test parameter spec builder`(specBuilder: () -> ParameterBuilder, expected: String) { + val parameterSpec = specBuilder.invoke().build() + assertNotNull(parameterSpec) + assertEquals(expected, parameterSpec.toString()) + } + + @Test + fun `test spec to builder conversation`() { + val parameterSpec = ParameterSpec.builder("amount", Int::class) + .nullable(true) + .initializer("%L", "10") + .annotation(AnnotationSpec.builder("nullable").build()) + .build() + val specAsBuilder = parameterSpec.toBuilder() + assertNotNull(parameterSpec) + assertEquals(parameterSpec.name, specAsBuilder.name) + assertEquals(parameterSpec.type, specAsBuilder.typeName) + assertEquals(parameterSpec.isNullable, specAsBuilder.nullable) + assertTrue { specAsBuilder.initializer!!.isNotEmpty() } + assertContentEquals(parameterSpec.annotations, specAsBuilder.specData.annotations) + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/property/ConstantPropertySpecTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/property/ConstantPropertySpecTest.kt new file mode 100644 index 00000000..9a6e614a --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/property/ConstantPropertySpecTest.kt @@ -0,0 +1,53 @@ +package net.theevilreaper.dartpoet.property + +import net.theevilreaper.dartpoet.property.consts.ConstantPropertySpec +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.lang.IllegalArgumentException +import java.util.stream.Stream + + +class ConstantPropertySpecTest { + + companion object { + + @JvmStatic + private fun invalidFileConstantSpec() = Stream.of( + Arguments.of( + "The name of a file constant can't be empty", + { ConstantPropertySpec.classConst("", String::class).build() } + ), + Arguments.of( + "The initializer can't be empty", + { ConstantPropertySpec.classConst("test", String::class).build() } + ), + Arguments.of( + "A file constant can't be private", + { ConstantPropertySpec.fileConst("test").initWith("%S", "test").private(true).build() } + ) + ) + } + + @ParameterizedTest + @MethodSource("invalidFileConstantSpec") + fun `test invalid file constant spec`(message: String, block: () -> ConstantPropertySpec) { + val exception = assertThrows { block() } + assertEquals(message, exception.message) + } + + @Test + fun `test toBuilder`() { + val builder = ConstantPropertySpec.classConst("test", String::class) + builder.initializer.add("%S", "test") + val spec = ConstantPropertySpec(builder) + val newBuilder = spec.toBuilder() + assertEquals(builder.name, newBuilder.name) + assertEquals(builder.typeName, newBuilder.typeName) + assertEquals(builder.initializer, newBuilder.initializer) + assertFalse { newBuilder.isPrivate } + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/property/PropertySpecTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/property/PropertySpecTest.kt new file mode 100644 index 00000000..ae795aa8 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/property/PropertySpecTest.kt @@ -0,0 +1,99 @@ +package net.theevilreaper.dartpoet.property + +import com.google.common.truth.Truth.assertThat +import net.theevilreaper.dartpoet.DartModifier +import net.theevilreaper.dartpoet.type.asTypeName +import net.theevilreaper.dartpoet.util.ALLOWED_CONST_MODIFIERS +import net.theevilreaper.dartpoet.util.ALLOWED_PROPERTY_MODIFIERS +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class PropertySpecTest { + + companion object { + + @JvmStatic + fun parameters(): Stream = Stream.of( + Arguments.of( + PropertySpec.builder("test", String::class.asTypeName().copy(nullable = true)).build(), + "String? test;" + ), + Arguments.of( + PropertySpec.builder("value", Int::class).build(), + "int value;" + ), + Arguments.of( + PropertySpec.builder("data", Int::class).initWith("%L", "4").build(), + "int data = 4;" + ), + Arguments.of( + PropertySpec.builder("data", Int::class).initWith("%L", "4").modifier { DartModifier.FINAL }.build(), + "final int data = 4;" + ), + Arguments.of( + PropertySpec.builder("id", String::class).modifiers(DartModifier.FINAL, DartModifier.PRIVATE) + .build(), + "final String _id;" + ) + ) + + @JvmStatic + private fun testInvalidPropertyCreation() = Stream.of( + Arguments.of( + { + PropertySpec.builder("test", String::class) + .modifier(DartModifier.REQUIRED) + .build() + }, + "These modifiers [REQUIRED] are not allowed in a property context. Allowed modifiers: $ALLOWED_PROPERTY_MODIFIERS" + ), + Arguments.of( + { PropertySpec.builder("", String::class).build() }, + "The name of a property can't be empty" + ), + Arguments.of( + { PropertySpec.constBuilder("test", String::class).build() }, + "A const variable needs an init block" + ), + Arguments.of( + { + PropertySpec.constBuilder("test") + .modifier(DartModifier.FINAL) + .build() + }, + "These modifiers [FINAL] are not allowed in a const property context. Allowed modifiers: $ALLOWED_CONST_MODIFIERS" + ) + ) + } + + @ParameterizedTest + @MethodSource("parameters") + fun `test properties`(propertySpec: PropertySpec, expected: String) { + assertThat(propertySpec.toString()).isEqualTo(expected) + } + + @ParameterizedTest + @MethodSource("testInvalidPropertyCreation") + fun `test property creation with invalid values`(block: () -> Unit, exceptionMessage: String) { + val exception = assertThrows { block() } + assertEquals(exceptionMessage, exception.message) + } + + @Test + fun `test spec to builder conversation`() { + val propertySpec = PropertySpec.builder("amount", Int::class.asTypeName().copy(nullable = true)) + .build() + val specAsBuilder = propertySpec.toBuilder().modifier(DartModifier.FINAL) + assertNotNull(specAsBuilder) + assertEquals(propertySpec.name, specAsBuilder.name) + assertEquals(propertySpec.type, specAsBuilder.type) + assertTrue { specAsBuilder.modifiers.isNotEmpty() } + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/type/ParameterizedTypeNameTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/type/ParameterizedTypeNameTest.kt new file mode 100644 index 00000000..68a86644 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/type/ParameterizedTypeNameTest.kt @@ -0,0 +1,80 @@ +package net.theevilreaper.dartpoet.type + +import net.theevilreaper.dartpoet.type.ParameterizedTypeName.Companion.parameterizedBy +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class ParameterizedTypeNameTest { + + companion object { + + @JvmStatic + private fun typeNameTest() = Stream.of( + Arguments.of("List", List::class.parameterizedBy(Int::class)), + Arguments.of( + "Map", + Map::class.parameterizedBy(String::class, String::class) + ), + Arguments.of( + "Map", + Map::class.parameterizedBy(String::class.asTypeName(), DYNAMIC) + ) + ) + + @JvmStatic + private fun typeNameCompanions() = Stream.of( + Arguments.of("TestClass", ClassName("TestClass").parameterizedBy(Int::class.asTypeName())), + Arguments.of("TestClass", ClassName("TestClass").parameterizedBy(Int::class)), + Arguments.of("List", List::class.java.parameterizedBy(Int::class.java)), + ) + + @JvmStatic + private fun testEnclosingTyeName() = Stream.of( + Arguments.of( + "Map.Entry", + ParameterizedTypeName(Map::class.asTypeName(), Map.Entry::class.asClassName(), typeArguments = listOf()) + ), + Arguments.of( + "Map.Entry", + ParameterizedTypeName( + Map::class.asTypeName(), + Map.Entry::class.asClassName(), + typeArguments = listOf(String::class.asTypeName(), Double::class.asTypeName()) + ) + ) + ) + } + + @ParameterizedTest + @MethodSource("typeNameTest") + fun `test parameterized type name class`(expected: String, parameter: ParameterizedTypeName) { + assertEquals(expected, parameter.toString()) + } + + @ParameterizedTest + @MethodSource("typeNameCompanions") + fun `test method from the parameterized companion object`(expected: String, parameter: ParameterizedTypeName) { + assertEquals(expected, parameter.toString()) + } + + @ParameterizedTest + @MethodSource("testEnclosingTyeName") + fun `test parameterized write with an enclosingTypeName`(expected: String, parameter: ParameterizedTypeName) { + assertEquals(expected, parameter.toString()) + } + + @Test + fun `test copy method from the parameterized TypeName`() { + val parameterizedTypeName = List::class.parameterizedBy(Int::class) + assertFalse { parameterizedTypeName.isNullable } + val nullableType = parameterizedTypeName.copy(nullable = true) + assertTrue { nullableType.isNullable } + val changedParameter = parameterizedTypeName.copy(nullable = true, listOf(Double::class.asClassName())) + assertNotEquals(parameterizedTypeName, changedParameter) + assertNotEquals(nullableType, changedParameter) + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/type/TypeNameExceptionsTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/type/TypeNameExceptionsTest.kt new file mode 100644 index 00000000..b63fec3d --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/type/TypeNameExceptionsTest.kt @@ -0,0 +1,52 @@ +package net.theevilreaper.dartpoet.type + +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream +import kotlin.test.assertEquals + +class TypeNameExceptionsTest { + + companion object { + + @JvmStatic + private fun copyWithException() = Stream.of( + Arguments.of("The dynamic type can't be copied", { DYNAMIC.copy() }), + ) + + @JvmStatic + private fun testTypeNameConditions() = Stream.of( + Arguments.of("The name of a ClassName can't be empty (includes only spaces)", { ClassName("") }), + Arguments.of("The name of a ClassName can't be empty (includes only spaces)", { ClassName(" ") }), + Arguments.of("no type arguments: List", { + ParameterizedTypeName( + enclosingTypeName = null, + rawType = List::class.asClassName(), + typeArguments = listOf() + ) + }), + ) + } + + @ParameterizedTest + @MethodSource("copyWithException") + fun `test copy method from class name implementation which throws an IllegalAccessException`( + exceptionMessage: String, + methodCall: () -> Unit + ) { + val exception = assertThrows { methodCall() } + assertEquals(exceptionMessage, exception.message) + } + + @ParameterizedTest + @MethodSource("testTypeNameConditions") + fun `test type name condition on object creation which triggers an exception`( + exceptionMessage: String, + objectCall: () -> Unit + ) { + val exception = assertThrows { objectCall() } + assertEquals(exceptionMessage, exception.message) + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/type/TypeNameTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/type/TypeNameTest.kt new file mode 100644 index 00000000..a4fcec70 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/type/TypeNameTest.kt @@ -0,0 +1,64 @@ +package net.theevilreaper.dartpoet.type + +import net.theevilreaper.dartpoet.clazz.ClassSpec +import net.theevilreaper.dartpoet.type.ParameterizedTypeName.Companion.parameterizedBy +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.lang.IllegalArgumentException +import java.lang.reflect.Type +import java.util.function.BiFunction +import java.util.stream.Stream + +class TypeNameTest { + + companion object { + + @JvmStatic + private fun primitiveTests() = Stream.of( + Arguments.of("int", Int::class.asTypeName()), + Arguments.of("int", Long::class.asClassName()), + Arguments.of("String", String::class.asTypeName()), + Arguments.of("double", Double::class.asTypeName()), + Arguments.of("double", Float::class.asClassName()), + Arguments.of("bool", Boolean::class.asTypeName()) + ) + } + + @ParameterizedTest + @MethodSource("primitiveTests") + fun `test primitive type conversation to a ClassName`(expectedType: String, type: TypeName) { + assertEquals(expectedType, type.toString()) + } + + @Test + fun `test get typeName with the java type and its an array`() { + val arrayType = intArrayOf() + val exception = assertThrows { TypeName.get(arrayType::class.java) } + assertEquals("An array type is not supported at the moment", exception.message) + } + + @Test + fun `test get typeName with the java type which is not an array`() { + val className = BiFunction::class.asClassName() + val typeNameFromMethod = TypeName.get(BiFunction::class.java) + assertEquals(className, typeNameFromMethod) + } + + @Test + fun `test parseSimpleKClass with kotlin type class`() { + val className = String::class.asClassName() + val typeNameFromMethod = TypeName.parseSimpleKClass(String::class) + assertEquals(className, typeNameFromMethod) + } + + @Test + fun `test parseSimpleKClass with raise an exception`() { + val type = ULong::class + val exception = assertThrows { TypeName.parseSimpleKClass(type) } + assertEquals("The given $type is not a primitive object", exception.message) + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/util/ConstantsTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/util/ConstantsTest.kt new file mode 100644 index 00000000..9ccb6687 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/util/ConstantsTest.kt @@ -0,0 +1,34 @@ +package net.theevilreaper.dartpoet.util + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream + +class ConstantsTest { + + companion object { + + @JvmStatic + private fun patterns() = Stream.of( + Arguments.of("", false), + Arguments.of("Dart_FILE", false), + Arguments.of("item_model", true), + Arguments.of("item_model.dart", true), + Arguments.of("model.dart", true), + Arguments.of("model_", false), + Arguments.of("boss_bar_colour_meep", true), + Arguments.of("hello__world.dart", false), + Arguments.of("_hello__world_.dart", false), + Arguments.of("test", true), + Arguments.of("_test", false) + ) + } + + @ParameterizedTest + @MethodSource("patterns") + fun `test name pattern`(name: String, result: Boolean) { + assertEquals(result, isDartConventionFileName(name)) + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/util/IndentTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/util/IndentTest.kt new file mode 100644 index 00000000..21e29706 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/util/IndentTest.kt @@ -0,0 +1,33 @@ +package net.theevilreaper.dartpoet.util + +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class IndentTest { + + companion object { + + @JvmStatic + private fun indentTest() = Stream.of( + Arguments.of(" ", true), + Arguments.of(" ", true), + Arguments.of("", false), + Arguments.of(" a", false), + Arguments.of("123", false), + ) + } + + @ParameterizedTest + @MethodSource("indentTest") + fun `test indent pattern`(indent: String, expected: Boolean) { + if (expected) { + assertTrue { isIndent(indent) } + } else { + assertFalse { isIndent(indent) } + } + } +} diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/util/LowerCamelCaseTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/util/LowerCamelCaseTest.kt new file mode 100644 index 00000000..c04fcc4f --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/util/LowerCamelCaseTest.kt @@ -0,0 +1,33 @@ +package net.theevilreaper.dartpoet.util + +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class LowerCamelCaseTest { + + companion object { + + @JvmStatic + private fun testSubjects() = Stream.of( + Arguments.of("TEST_VALUE", false), + Arguments.of("value", true), + Arguments.of("NiceValue", false), + Arguments.of("12121121", false), + Arguments.of("value1", true) + ) + } + + @ParameterizedTest + @MethodSource("testSubjects") + fun `test lower camel case subjects`(input: String, expected: Boolean) { + if (expected) { + assertTrue(isInLowerCamelCase(input)) + } else { + assertFalse(isInLowerCamelCase(input)) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/theevilreaper/dartpoet/util/StringHelperTest.kt b/src/test/kotlin/net/theevilreaper/dartpoet/util/StringHelperTest.kt new file mode 100644 index 00000000..a65b7464 --- /dev/null +++ b/src/test/kotlin/net/theevilreaper/dartpoet/util/StringHelperTest.kt @@ -0,0 +1,24 @@ +package net.theevilreaper.dartpoet.util + +import net.theevilreaper.dartpoet.DartModifier +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class StringHelperTest { + + @Test + fun `test modifier concatenation`() { + val modifiers = setOf(DartModifier.CONST, DartModifier.FINAL, DartModifier.FACTORY) + val result = StringHelper.joinModifiers(modifiers, separator = SPACE) + assertEquals("const final factory", result) + assertEquals(EMPTY_STRING, StringHelper.joinModifiers(emptySet())) + } + + @Test + fun `test ensure variable name with private modifier`() { + val name = "test" + val result = StringHelper.ensureVariableNameWithPrivateModifier(name,true) + assertEquals("_$name", result) + assertEquals(name, StringHelper.ensureVariableNameWithPrivateModifier(name, false)) + } +} \ No newline at end of file