diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..75d15515 --- /dev/null +++ b/.editorconfig @@ -0,0 +1 @@ +[*.{kt,kts}] \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..fe30e00d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# Dependabot configuration: +# https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + # Maintain dependencies for Gradle dependencies + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "daily" + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..2e1aa07c --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,84 @@ +name-template: 'v$RESOLVED_VERSION 🌈' +tag-template: 'v$RESOLVED_VERSION' +categories: + - title: 'New Features' + labels: + - 'feature' + - 'enhancement' + - title: 'Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - title: 'Maintenance' + labels: + - 'chore' + - 'ci' + - 'perf' + - 'refactor' + - 'security' + - 'test' + - title: 'Documentation' + labels: + - 'docs' + - title: 'Dependency Updates' + labels: + - 'dependencies' + +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +exclude-labels: + - 'skip-changelog' + +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + - 'feat' + - 'feature' + patch: + labels: + - 'patch' + default: patch + +autolabeler: + - label: 'chore' + branch: + - '/chore\/.+/' + - label: 'ci' + branch: + - '/ci\/.+/' + - label: 'dependencies' + branch: + - '/dependencies\/.+/' + - label: 'docs' + branch: + - '/docs\/.+/' + - label: 'feature' + branch: + - '/feat(ure)?\/.+/' + - label: 'fix' + branch: + - '/fix\/.+/' + - label: 'perf' + branch: + - '/perf\/.+/' + - label: 'refactor' + branch: + - '/refactor\/.+/' + - label: 'security' + branch: + - '/security\/.+/' + - label: 'test' + branch: + - '/test\/.+/' + +template: | + # What's Changed + + $CHANGES + + **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...$RESOLVED_VERSION \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..00f54bf6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,286 @@ +# GitHub Actions Workflow is created for testing and preparing the plugin release in the following steps: +# - Validate Gradle Wrapper. +# - Run 'test' and 'verifyPlugin' tasks. +# - Run Qodana inspections. +# - Run the 'buildPlugin' task and prepare artifact for further tests. +# - Run the 'runPluginVerifier' task. +# - Create a draft release. +# +# The workflow is triggered on push and pull_request events. +# +# GitHub Actions reference: https://help.github.com/en/actions +# +## JBIJPPTPL + +name: Build +on: + # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g., for dependabot pull requests) + push: + branches: [ main ] + # Trigger the workflow on any pull request + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + + # Prepare environment and build the plugin + build: + name: Build + runs-on: ubuntu-22.04 + outputs: + version: ${{ steps.properties.outputs.version }} + changelog: ${{ steps.properties.outputs.changelog }} + pluginVerifierHomeDir: ${{ steps.properties.outputs.pluginVerifierHomeDir }} + steps: + + # Check out the current repository + - name: Fetch Sources + uses: actions/checkout@v4 + + # Validate wrapper + - name: Gradle Wrapper Validation + uses: gradle/actions/wrapper-validation@v4 + + # Set up Java environment for the next steps + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + + # Setup Gradle + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + # Set environment variables + - name: Export Properties + id: properties + shell: bash + run: | + PROPERTIES="$(./gradlew properties --console=plain -q)" + VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" + CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" + + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "pluginVerifierHomeDir=~/.pluginVerifier" >> $GITHUB_OUTPUT + + echo "changelog<> $GITHUB_OUTPUT + echo "$CHANGELOG" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + # Generate Lexer and Parser + - name: Generate Lexer + run: ./gradlew generateLexer + + - name: Generate Parser + run: ./gradlew generateParser + + # Build plugin + - name: Build plugin + run: ./gradlew buildPlugin -PpluginVersion=0.3.0 + + # Prepare plugin archive content for creating artifact + - name: Prepare Plugin Artifact + id: artifact + shell: bash + run: | + cd ${{ github.workspace }}/build/distributions + FILENAME=`ls *.zip` + unzip "$FILENAME" -d content + + echo "filename=${FILENAME:0:-4}" >> $GITHUB_OUTPUT + + # Store already-built plugin as an artifact for downloading + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.artifact.outputs.filename }} + path: ./build/distributions/content/*/* + + # Run tests and upload a code coverage report + test: + name: Test + needs: [ build ] + runs-on: ubuntu-22.04 + steps: + + # Check out the current repository + - name: Fetch Sources + uses: actions/checkout@v4 + + # Set up Java environment for the next steps + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + + # Setup Gradle + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + # Generate Lexer and Parser + - name: Generate Lexer + run: ./gradlew generateLexer + + - name: Generate Parser + run: ./gradlew generateParser + + # Run tests + - name: Run Tests + run: ./gradlew check + + # Collect Tests Result of failed tests + - name: Collect Tests Result + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: tests-result + path: ${{ github.workspace }}/build/reports/tests + + # Upload the Kover report to CodeCov + - name: Upload Code Coverage Report + uses: codecov/codecov-action@v5 + with: + files: ${{ github.workspace }}/build/reports/kover/report.xml + + # Run Qodana inspections and provide report + inspectCode: + name: Inspect code + needs: [ build ] + runs-on: ubuntu-22.04 + permissions: + contents: write + checks: write + pull-requests: write + steps: + + # Free GitHub Actions Environment Disk Space + - name: Maximize Build Space + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + large-packages: false + + # Check out the current repository + - name: Fetch Sources + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit + fetch-depth: 0 # a full history is required for pull request analysis + + # Set up Java environment for the next steps + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + + # Generate Lexer and Parser + - name: Generate Lexer + run: ./gradlew generateLexer + + - name: Generate Parser + run: ./gradlew generateParser + + # Run Qodana inspections + - name: Qodana - Code Inspection + uses: JetBrains/qodana-action@v2024.3 + with: + cache-default-branch-only: true + upload-result: true + + # Run plugin structure verification along with IntelliJ Plugin Verifier + verify: + name: Verify plugin + needs: [ build ] + runs-on: ubuntu-22.04 + steps: + + # Free GitHub Actions Environment Disk Space + - name: Maximize Build Space + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + large-packages: false + + # Check out the current repository + - name: Fetch Sources + uses: actions/checkout@v4 + + # Set up Java environment for the next steps + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + + # Setup Gradle + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + # Generate Lexer and Parser + - name: Generate Lexer + run: ./gradlew generateLexer + + - name: Generate Parser + run: ./gradlew generateParser + + # Cache Plugin Verifier IDEs + - name: Setup Plugin Verifier IDEs Cache + uses: actions/cache@v4 + with: + path: ${{ needs.build.outputs.pluginVerifierHomeDir }}/ides + key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }} + + # Run Verify Plugin task and IntelliJ Plugin Verifier tool + - name: Run Plugin Verification tasks + run: ./gradlew verifyPlugin -Dplugin.verifier.home.dir=${{ needs.build.outputs.pluginVerifierHomeDir }} + + # Collect Plugin Verifier Result + - name: Collect Plugin Verifier Result + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: pluginVerifier-result + path: ${{ github.workspace }}/build/reports/pluginVerifier + + # Prepare a draft release for GitHub Releases page for the manual verification + # If accepted and published, release workflow would be triggered + releaseDraft: + name: Release draft + if: github.event_name != 'pull_request' + needs: [ build, test, inspectCode, verify ] + runs-on: ubuntu-22.04 + permissions: + contents: write + steps: + + # Check out the current repository + - name: Fetch Sources + uses: actions/checkout@v4 + + # Remove old release drafts by using the curl request for the available releases with a draft flag + - name: Remove Old Release Drafts + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh api repos/{owner}/{repo}/releases \ + --jq '.[] | select(.draft == true) | .id' \ + | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} + + # Create a new release draft which is not publicly visible and requires manual acceptance + - name: Create Release Draft + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "v${{ needs.build.outputs.version }}" \ + --draft \ + --title "v${{ needs.build.outputs.version }}" \ + --notes "$(cat << 'EOM' + ${{ needs.build.outputs.changelog }} + EOM + )" diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 00000000..17e5c5fc --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,24 @@ +name: Release Drafter + +on: + push: + branches: + - main + + pull_request: + types: [opened, reopened, synchronize] + workflow_dispatch: + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ae110cca --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,108 @@ +# GitHub Actions Workflow created for handling the release process based on the draft release prepared with the Build workflow. +# Running the publishPlugin task requires all following secrets to be provided: PUBLISH_TOKEN, PRIVATE_KEY, PRIVATE_KEY_PASSWORD, CERTIFICATE_CHAIN. +# See https://plugins.jetbrains.com/docs/intellij/plugin-signing.html for more information. + +name: Release +on: + release: + types: [prereleased, released] + +jobs: + + # Prepare and publish the plugin to JetBrains Marketplace repository + release: + name: Publish Plugin + runs-on: ubuntu-22.04 + permissions: + contents: write + pull-requests: write + steps: + + # Check out the current repository + - name: Fetch Sources + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name }} + + # Set up Java environment for the next steps + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + + # Setup Gradle + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + # Set environment variables + - name: Export Properties + id: properties + shell: bash + run: | + CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d' + ${{ github.event.release.body }} + EOM + )" + + echo "changelog<> $GITHUB_OUTPUT + echo "$CHANGELOG" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + # Update the Unreleased section with the current release note + - name: Patch Changelog + if: ${{ steps.properties.outputs.changelog != '' }} + env: + CHANGELOG: ${{ steps.properties.outputs.changelog }} + run: | + ./gradlew patchChangelog --release-note="$CHANGELOG" + + # Generate Lexer and Parser + - name: Generate Lexer + run: ./gradlew generateLexer + + - name: Generate Parser + run: ./gradlew generateParser + + # Publish the plugin to JetBrains Marketplace + - name: Publish Plugin + env: + PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} + CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }} + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }} + run: ./gradlew publishPlugin + + # Upload artifact as a release asset + - name: Upload Release Asset + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/* + + # Create a pull request + - name: Create Pull Request + if: ${{ steps.properties.outputs.changelog != '' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ github.event.release.tag_name }}" + BRANCH="changelog-update-$VERSION" + LABEL="release changelog" + + git config user.email "action@github.com" + git config user.name "GitHub Action" + + git checkout -b $BRANCH + git commit -am "Changelog update - $VERSION" + git push --set-upstream origin $BRANCH + + gh label create "$LABEL" \ + --description "Pull requests with release changelog update" \ + --force \ + || true + + gh pr create \ + --title "Changelog update - \`$VERSION\`" \ + --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \ + --label "$LABEL" \ + --head $BRANCH diff --git a/.github/workflows/run-ui-tests.yml b/.github/workflows/run-ui-tests.yml new file mode 100644 index 00000000..24f151cc --- /dev/null +++ b/.github/workflows/run-ui-tests.yml @@ -0,0 +1,63 @@ +# GitHub Actions Workflow for launching UI tests on Linux, Windows, and Mac in the following steps: +# - Prepare and launch IDE with your plugin and robot-server plugin, which is needed to interact with the UI. +# - Wait for IDE to start. +# - Run UI tests with a separate Gradle task. +# +# Please check https://github.com/JetBrains/intellij-ui-test-robot for information about UI tests with IntelliJ Platform. +# +# Workflow is triggered manually. + +name: Run UI Tests +on: + workflow_dispatch + +jobs: + + testUI: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-22.04 + runIde: | + export DISPLAY=:99.0 + Xvfb -ac :99 -screen 0 1920x1080x16 & + gradle runIdeForUiTests & + - os: windows-latest + runIde: start gradlew.bat runIdeForUiTests + - os: macos-latest + runIde: ./gradlew runIdeForUiTests & + + steps: + + # Check out the current repository + - name: Fetch Sources + uses: actions/checkout@v4 + + # Set up Java environment for the next steps + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + + # Setup Gradle + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + # Run IDEA prepared for UI testing + - name: Run IDE + run: ${{ matrix.runIde }} + + # Wait for IDEA to be started + - name: Health Check + uses: jtalk/url-health-check-action@v4 + with: + url: http://127.0.0.1:8082 + max-attempts: 15 + retry-delay: 30s + + # Run tests + - name: Tests + run: ./gradlew test diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..869be84b --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.gradle +.idea +.intellijPlatform +.qodana +build +.run/ +out/ +.../*.jar +/src/main/gen/ +/java_pid**.hprof +*.iml +*.ipr +*.iws +.kotlin +secrets.properties +/certificate/** \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 00000000..f2c1963c --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/.run/Run Plugin.run.xml b/.run/Run Plugin.run.xml new file mode 100644 index 00000000..0e6ea839 --- /dev/null +++ b/.run/Run Plugin.run.xml @@ -0,0 +1,25 @@ + + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.run/Run Tests.run.xml b/.run/Run Tests.run.xml new file mode 100644 index 00000000..f281bdc8 --- /dev/null +++ b/.run/Run Tests.run.xml @@ -0,0 +1,25 @@ + + + + + + + + true + true + false + true + + + diff --git a/.run/Run Verifications.run.xml b/.run/Run Verifications.run.xml new file mode 100644 index 00000000..32783f57 --- /dev/null +++ b/.run/Run Verifications.run.xml @@ -0,0 +1,25 @@ + + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..8cff777d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,38 @@ +# Doma Tools for IntelliJ + +## [Unreleased] + +## [0.3.0] - 2025-03-07 + +### Dao Support Features + +- **Actions** + - **Jump to SQL:** Added an action (with a gutter icon) that jumps to the SQL file from the Dao method. + - Shortcut key: Alt+D + - **Generate SQL:** Added an action to generate SQL files. + - Shortcut key: Ctrl+Alt+G +- **Code Inspection** + - Displays a quick fix when the corresponding SQL template file for a Dao method requiring one is not found. + - Shows an error if there are parameter arguments not used as SQL bind variables. + +### SQL Support Features + +- **Actions** + - **Jump to Dao:** Added an action to jump from the SQL file to the Dao method. + - Shortcut key: Alt+D + - **Jump to Declaration:** Added an action to jump from SQL bind variables to Dao parameters or class definitions. + - Shortcut key: Alt+E +- **Code Inspection** + - Displays an error when fields or methods that do not exist in the Dao parameters or class definition are used. +- **Code Completion** + - Provides code completion for Dao method parameters, instance fields, and methods when used as bind variables. + - Provides code completion for static fields and methods during static property calls. + - Offers code completion for directive names. + - Provides code completion for Doma built-in functions. +- **Refactoring** + - Rename SQL file when renaming Dao method + - Rename SQL file directory when renaming Dao + - Change Dao package name or SQL file directory configuration when changing configuration + +[Unreleased]: https://github.com/domaframework/doma-tools-for-intellij/commits/main +[0.3.0]: https://github.com/domaframework/doma-tools-for-intellij/compare/v0.3.0 diff --git a/README.md b/README.md index 89797301..b5df5d4a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,64 @@ -# doma-tools-for-intellij -Doma Tools for IntelliJ IDEA +# Doma Tools for IntelliJ + + +“Doma Tools” is a plugin that supports the development of Doma-based projects in IntelliJ. + +It checks associations between Dao and SQL, and offers coding support features for Doma syntax, +such as generating SQL template files, navigating between files, and inspections to ensure the validity of bind variables. + + +## Features + +## Actions +The plugin adds some actions, gutter icons. +Shortcut keys can be used for actions + +![action.png](images/action.png) + +- **Jump to SQL(Alt+D)** + - Jump to action from Dao to SQL + - You can also jump to the SQL file from the gutter icon that is displayed together. +- **Generate SQL(Ctrl+Alt+G)** + - Generate SQL file +- **Jump to Dao(Alt+D)** + - Jump to action from SQL to Dao + - You can also jump to the Dao Method from the gutter icon that is displayed together. +- **Jump to Declaration(Alt+E)** + - Jump to action from SQL bind variable to declaration location + - ex: Dao arguments, fields, method declaration + +## Inspection +Check that bind variables are used appropriately for Dao and SQL associations. +The plugin also provides quick fixes for Dao methods where the required SQL files do not exist. + +- Quick fix for missing SQL template file + ![quickfix.png](images/quickfix.png) +- Checking for Dao method arguments not used in bind variables + ![inspection.png](images/inspection.png) + +## Completion +Adds code completion functionality to support indexing of Doma directives and bind variables + +- Suggest Dao method arguments in bind variable directives + ![complete_bindVariables.png](images/complete_bindVariables.png) +- Refer to class definition from Dao method argument type and suggest fields and methods + ![complete_member.png](images/cpmplete_member.png) +- Suggest members defined as static in static fields and method calls +- Suggest Doma directives +- Directives such as Condition, Loop, Population are suggested after “%” +- Suggest built-in functions after “@” + +## Refactoring +Along with the Dao name change, we will refactor the SQL file directory and file name. + +- After refactoring the Dao name, change the SQL deployment directory name as well. +- After refactoring the Dao method name, we will also change the SQL file name. +- After refactoring the Dao package, we will also change the SQL directory. + +## Settings +Some functions of "Doma Tools" can be customized from the settings screen. + +- Enabling/disabling inspections and customizing error levels +- Highlight color settings for SQL elements +![setting_highlight.png](images/setting_highlight.png) +- Customize action shortcut keys diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..8ecc6a7a --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,255 @@ +import org.gradle.internal.classpath.Instrumented.systemProperty +import org.jetbrains.changelog.Changelog +import org.jetbrains.changelog.markdownToHTML +import org.jetbrains.intellij.platform.gradle.TestFrameworkType +import java.util.Base64 + +plugins { + id("java") + alias(libs.plugins.spotless) + alias(libs.plugins.kotlin) + alias(libs.plugins.intelliJPlatform) + alias(libs.plugins.changelog) + alias(libs.plugins.qodana) + alias(libs.plugins.kover) + alias(libs.plugins.grammarkit) +} + +grammarKit { + tasks { + generateLexer { + group = "grammer-kit" + sourceFile = file("src/main/java/org/domaframework/doma/intellij/Sql.flex") + targetOutputDir = file("src/main/gen/org/domaframework/doma/intellij") + purgeOldFiles = true + } + generateParser { + group = "grammer-kit" + sourceFile = file("src/main/java/org/domaframework/doma/intellij/Sql.bnf") + targetRootOutputDir = file("src/main/gen") + pathToParser = "/org/domaframework/doma/intellij/SqlParser.java" + pathToPsiRoot = "/org/domaframework/doma/intellij/psi" + purgeOldFiles = true + } + } +} + +group = providers.gradleProperty("pluginGroup").get() +version = providers.gradleProperty("pluginVersion").get() + +kotlin { + jvmToolchain(17) +} + +repositories { + gradlePluginPortal() + google() + mavenCentral() + + // IntelliJ Platform Gradle Plugin Repositories Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-repositories-extension.html + intellijPlatform { + defaultRepositories() + } +} + +sourceSets { + main { + java { + sourceSets["main"].java.srcDirs("src/main/gen") + sourceSets["main"].kotlin.srcDirs("src/main/kotlin") + } + } + + test { + systemProperty("user.language", "ja") + } +} + +dependencies { + implementation(libs.slf4j) + implementation(libs.logback) + + testImplementation(libs.junit) + testImplementation(libs.kotlinTest) + testImplementation(libs.domacore) + + intellijPlatform { + create(providers.gradleProperty("platformType"), providers.gradleProperty("platformVersion")) + + bundledPlugins(providers.gradleProperty("platformBundledPlugins").map { it.split(',') }) + + plugins(providers.gradleProperty("platformPlugins").map { it.split(',') }) + + pluginVerifier() + zipSigner() + testFramework(TestFrameworkType.Platform) + testFramework(TestFrameworkType.Plugin.Java) + testFramework(TestFrameworkType.Metrics) + } +} + +intellijPlatform { + pluginConfiguration { + version = providers.gradleProperty("pluginVersion") + description = + providers.fileContents(layout.projectDirectory.file("README.md")).asText.map { + val start = "" + val end = "" + + with(it.lines()) { + if (!containsAll(listOf(start, end))) { + throw GradleException("Plugin description section not found in README.md:\n$start ... $end") + } + subList(indexOf(start) + 1, indexOf(end)).joinToString("\n").let(::markdownToHTML) + } + } + + val changelog = project.changelog + changeNotes = + providers.gradleProperty("pluginVersion").map { pluginVersion -> + with(changelog) { + renderItem( + (getOrNull(pluginVersion) ?: getUnreleased()) + .withHeader(false) + .withEmptySections(false), + Changelog.OutputType.HTML, + ) + } + } + + ideaVersion { + sinceBuild = providers.gradleProperty("pluginSinceBuild") + untilBuild = providers.gradleProperty("pluginUntilBuild") + } + } + + signing { + certificateChain = providers.environmentVariable("CERTIFICATE_CHAIN") + privateKey = providers.environmentVariable("PRIVATE_KEY") + password = providers.environmentVariable("PRIVATE_KEY_PASSWORD") + } + + publishing { + token = providers.environmentVariable("PUBLISH_TOKEN") + channels = + providers.gradleProperty("pluginVersion").map { listOf(it.substringAfter('-', "").substringBefore('.').ifEmpty { "default" }) } + } + + pluginVerification { + ides { + recommended() + } + } +} + +// Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin +changelog { + groups.empty() + repositoryUrl = providers.gradleProperty("pluginRepositoryUrl") +} + +// Configure Gradle Kover Plugin - read more: https://github.com/Kotlin/kotlinx-kover#configuration +kover { + reports { + total { + xml { + onCheck = true + } + } + } +} + +tasks { + wrapper { + gradleVersion = providers.gradleProperty("gradleVersion").get() + } + + publishPlugin { + dependsOn(patchChangelog) + } +} + +tasks.register("encodeBase64") { + doLast { + val currentDir = File("./certificate") + val files = currentDir.listFiles() ?: return@doLast + + for (file in files) { + val fileName = file.name + + if (fileName == "build.gradle.kts" || fileName.endsWith(".example")) continue + + val fileData = file.readBytes() + val encodedData = Base64.getEncoder().encodeToString(fileData) + + println("File Name: $fileName") + println("Encoded Data:") + println(encodedData) + println("\n\n\n") + } + } +} + +intellijPlatformTesting { + runIde { + register("runIdeForUiTests") { + task { + jvmArgumentProviders += + CommandLineArgumentProvider { + listOf( + "-Drobot-server.port=8082", + "-Dide.mac.message.dialogs.as.sheets=false", + "-Djb.privacy.policy.text=", + "-Djb.consents.confirmation.enabled=false", + "-Didea.log.registry.conflicts.silent=true", + ) + } + } + + plugins { + robotServerPlugin() + } + } + } +} + +spotless { + val targetExclude = + listOf( + "src/test/kotlin/org/domaframework/doma/intellij/*.kt", + "src/test/testData/**", + "src/main/gen/**", + ) + val licenseHeaderFile = rootProject.file("spotless/copyright.java") + + lineEndings = com.diffplug.spotless.LineEnding.UNIX + java { + targetExclude(targetExclude) + googleJavaFormat( + libs.google.java.format + .get() + .version, + ) + licenseHeaderFile(licenseHeaderFile) + } + // https://github.com/diffplug/spotless/issues/532 + format("javaMisc") { + targetExclude(targetExclude) + target("src/*.java") + licenseHeaderFile(licenseHeaderFile, "(package|module|\\/\\*\\*)") + } + kotlin { + ktlint(libs.ktlint.get().version) + licenseHeaderFile(licenseHeaderFile) + } + kotlinGradle { + ktlint(libs.ktlint.get().version) + } + format("misc") { + target("**/*.gitignore", "docs/**/*.rst", "**/*.md") + targetExclude("**/bin/**", "**/build/**") + leadingTabsToSpaces() + trimTrailingWhitespace() + endWithNewline() + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..35c17227 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,22 @@ +pluginGroup = org.domaframework.doma.intellij +pluginName = Doma Tools for IntelliJ +pluginRepositoryUrl = https://github.com/domaframework/doma-tools-for-intellij +pluginVersion = 0.3.0 + +pluginSinceBuild=231 + +platformType = IC +platformVersion = 2024.3.1 + +platformPlugins = +platformBundledPlugins = com.intellij.java,org.jetbrains.kotlin + +gradleVersion = 8.10.2 + +kotlin.stdlib.default.dependency = false + +org.gradle.configuration-cache = true + +org.gradle.caching = true + +updateSinceUntilBuild = false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..1b4f624a --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,31 @@ +[versions] +# libraries +junit = "4.13.2" +doma = "3.2.0" +logback = "1.5.16" + +# plugins +changelog = "2.2.1" +intelliJPlatform = "2.2.1" +kotlin = "2.1.10" +kover = "0.9.1" +qodana = "2024.3.4" +grammarkit = "2022.3.2.2" + +[libraries] +junit = { group = "junit", name = "junit", version.ref = "junit" } +logback = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" } +slf4j = { group = "org.slf4j", name = "slf4j-api", version = "2.0.16" } +kotlinTest = { group = "org.jetbrains.kotlin", name = "kotlin-test" } +domacore = { module = "org.seasar.doma:doma-core", version.ref = "doma" } +google-java-format = { module = "com.google.googlejavaformat:google-java-format", version = "1.25.2" } +ktlint = { module = "com.pinterest.ktlint:ktlint-cli", version = "1.5.0" } + +[plugins] +changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } +intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "intelliJPlatform" } +kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } +qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" } +grammarkit= { id = "org.jetbrains.grammarkit", version.ref = "grammarkit" } +spotless = { id = "com.diffplug.spotless", version = "7.0.2" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..a4b76b95 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..df97d72b --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..f5feea6d --- /dev/null +++ b/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + 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. +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=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# 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" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..9b42019c --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@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. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +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/images/action.png b/images/action.png new file mode 100644 index 00000000..32b1cf51 Binary files /dev/null and b/images/action.png differ diff --git a/images/complete_bindVariables.png b/images/complete_bindVariables.png new file mode 100644 index 00000000..5ef903e6 Binary files /dev/null and b/images/complete_bindVariables.png differ diff --git a/images/cpmplete_member.png b/images/cpmplete_member.png new file mode 100644 index 00000000..5e15d4ad Binary files /dev/null and b/images/cpmplete_member.png differ diff --git a/images/inspection.png b/images/inspection.png new file mode 100644 index 00000000..e3189c40 Binary files /dev/null and b/images/inspection.png differ diff --git a/images/quickfix.png b/images/quickfix.png new file mode 100644 index 00000000..cfbad1c0 Binary files /dev/null and b/images/quickfix.png differ diff --git a/images/setting_highlight.png b/images/setting_highlight.png new file mode 100644 index 00000000..bef1a545 Binary files /dev/null and b/images/setting_highlight.png differ diff --git a/qodana.yml b/qodana.yml new file mode 100644 index 00000000..a8dd8a89 --- /dev/null +++ b/qodana.yml @@ -0,0 +1,14 @@ +# Qodana configuration: +# https://www.jetbrains.com/help/qodana/qodana-yaml.html + +version: 1.0 +linter: jetbrains/qodana-jvm-community:2024.3 +projectJDK: "17" +profile: + name: qodana.recommended +exclude: + - name: All + paths: + - .qodana + - /src/main/gen/** + - /src/test/** diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..284f947c --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,5 @@ +rootProject.name = "Doma Tools for IntelliJ" + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" +} diff --git a/spotless/copyright.java b/spotless/copyright.java new file mode 100644 index 00000000..5f3ea7f0 --- /dev/null +++ b/spotless/copyright.java @@ -0,0 +1,15 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ \ No newline at end of file diff --git a/src/main/java/org/domaframework/doma/intellij/Sql.bnf b/src/main/java/org/domaframework/doma/intellij/Sql.bnf new file mode 100644 index 00000000..308a270b --- /dev/null +++ b/src/main/java/org/domaframework/doma/intellij/Sql.bnf @@ -0,0 +1,154 @@ +{ + parserClass="org.domaframework.doma.intellij.SqlParser" + parserUtilClass="org.domaframework.doma.intellij.setting.SqlParserUtil" + + extends="com.intellij.extapi.psi.ASTWrapperPsiElement" + + psiClassPrefix="Sql" + psiImplClassSuffix="Impl" + psiPackage="org.domaframework.doma.intellij.psi" + psiImplPackage = "org.domaframework.doma.intellij.impl" + + elementTypeHolderClass = "org.domaframework.doma.intellij.psi.SqlTypes" + elementTypeClass="org.domaframework.doma.intellij.psi.SqlElementType" + tokenTypeClass = "org.domaframework.doma.intellij.psi.SqlTokenType" + + //noinspection RegExpRedundantEscape + tokens=[ + STRING + NUMBER + KEYWORD + WORD + OTHER + BLOCK_COMMENT_START = "/*" + BLOCK_COMMENT_END = "*/" + BLOCK_COMMENT_CONTENT + LINE_COMMENT + + EL_IF = "%if" + EL_ELSEIF = "%elseif" + EL_ELSE = "%else" + EL_FOR = "%for" + EL_EXPAND = "%expand" + EL_POPULATE = "%populate" + EL_PARSER_LEVEL_COMMENT = "%!" + EL_END = "%end" + EL_HASH = "#" + EL_CARET = "^" + EL_SEPARATOR = ":" + EL_NULL = "null" + EL_BOOLEAN = "regexp:(true|false)" + EL_DOT = "." + EL_PLUS = "+" + EL_MINUS = "-" + EL_ASTERISK = "*" + EL_SLASH = "/" + EL_PERCENT = "%" + EL_EQ = "==" + EL_NE = "!=" + EL_LT = "<" + EL_LE = "<=" + EL_GT = ">" + EL_GE = ">=" + EL_NOT = "!" + EL_AND = "&&" + EL_OR = "||" + EL_COMMA = "," + EL_LEFT_PAREN = "(" + EL_RIGHT_PAREN = ")" + EL_AT_SIGN = "@" + EL_NEW = "new" + EL_NUMBER = "regexp:\d+(L|(\.\d+)?[FDB])?" + EL_STRING = "regexp:\"([^\"\\]|\\\"|\\)*\"" + EL_CHAR = "regexp:'([^\'\\]|\\\')'" + EL_IDENTIFIER = "regexp:\w+" + ] + + extends("el_literal_expr|el_id_expr|el_paren_expr")=el_primary_expr + extends("el_invocation_expr_group|el_primary_expr")=el_factor_expr + extends("el_logical_expr_group|el_factor_expr")=el_term_expr + extends(".*expr")=el_expr + consumeTokenMethod("literal|word|.*directive|.*expr")="consumeTokenFast" +} + +sql_file ::= content * + +private content ::= !<> item {pin=1 recoverWhile=content_recover} +private item ::= (comment | literal | word | OTHER) +private comment ::= (block_comment | LINE_COMMENT) +private literal ::= (STRING | NUMBER) +private word ::= (KEYWORD | WORD) +block_comment ::= "/*" (el_directive | BLOCK_COMMENT_CONTENT?) "*/" { + pin=1 + mixin="org.domaframework.doma.intellij.psi.SqlElCommentExprImpl" +} +// Detect errors for each item +private content_recover ::= !item + +private el_directive ::= el_bind_variable_directive + | el_literal_variable_directive + | el_embedded_variable_directive + | el_percent_directive + | el_if_directive + | el_elseif_directive + | el_else_directive + | el_for_directive + | el_end_directive + | el_expand_directive + | el_populate_directive + | el_parser_level_comment_directive + +private el_bind_variable_directive ::= el_expr +private el_literal_variable_directive ::= "^" el_expr {pin=1} +private el_embedded_variable_directive ::= "#" el_expr {pin=1} +private el_percent_directive ::= EL_PERCENT STRING {pin=1} +el_if_directive ::= "%if" el_expr {pin=1} +el_elseif_directive ::= "%elseif" el_expr {pin=1} +private el_else_directive ::= "%else" +el_for_directive ::= "%for" el_id_expr ":" el_expr {pin=1} +private el_end_directive ::= "%end" +private el_expand_directive ::= "%expand" el_expr? {pin=1} +private el_populate_directive ::= "%populate" +private el_parser_level_comment_directive ::= "%!" BLOCK_COMMENT_CONTENT* {pin=1} + +// expr +el_expr ::= el_logical_expr_group | el_term_expr +private el_logical_expr_group ::= el_not_expr | el_and_expr | el_or_expr +el_not_expr ::= "!" el_expr {pin=1} +el_and_expr ::= el_expr "&&" el_expr +el_or_expr ::= el_expr "||" el_expr + +// term +el_term_expr ::= el_comparison_expr_group | el_arithmetic_expr_group | el_factor_expr +private el_comparison_expr_group ::= el_eq_expr | el_ne_expr | el_lt_expr | el_le_expr | el_gt_expr | el_ge_expr +private el_arithmetic_expr_group ::= el_add_expr | el_subtract_expr | el_multiply_expr | el_divide_expr | el_mod_expr +el_eq_expr ::= el_factor_expr "==" el_factor_expr +el_ne_expr ::= el_factor_expr "!=" el_factor_expr +el_lt_expr ::= el_factor_expr "<" el_factor_expr +el_le_expr ::= el_factor_expr "<=" el_factor_expr +el_gt_expr ::= el_factor_expr ">" el_factor_expr +el_ge_expr ::= el_factor_expr ">=" el_factor_expr +el_add_expr ::= el_factor_expr "+" el_term_expr +el_subtract_expr ::= el_factor_expr "-" el_term_expr +el_multiply_expr ::= el_factor_expr "*" el_term_expr +el_divide_expr ::= el_factor_expr "/" el_term_expr +el_mod_expr ::= el_factor_expr "%" el_term_expr + +// factor +el_factor_expr ::= el_invocation_expr_group | el_primary_expr +private el_invocation_expr_group ::= el_static_field_access_expr | el_field_access_expr| el_function_call_expr | el_new_expr +el_static_field_access_expr ::= el_static_member_access +el_function_call_expr ::= !el_class "@" el_id_expr el_parameters +el_new_expr ::= "new" el_class el_parameters {pin=1} +private el_static_member_access ::= el_class_ref el_id_expr (el_parameters)? ("." (el_id_expr el_parameters?))* {pin=1} +el_field_access_expr ::= el_instance_member_access +private el_instance_member_access ::= el_primary_expr ("." (el_id_expr (el_parameters)?))+ +private el_class_ref ::= "@" el_class "@" +el_class ::= el_id_expr ("." el_id_expr)* +el_parameters ::= "(" (el_expr ("," el_expr)*)? ")" {pin=1} + +// primary +el_primary_expr ::= el_literal_expr | el_id_expr | el_paren_expr +private el_literal_expr ::= EL_NULL | EL_BOOLEAN | (EL_PLUS | EL_MINUS)? EL_NUMBER | EL_STRING | EL_CHAR +private el_id_expr ::= EL_IDENTIFIER +private el_paren_expr ::= "(" el_expr ")" {pin=1} \ No newline at end of file diff --git a/src/main/java/org/domaframework/doma/intellij/Sql.flex b/src/main/java/org/domaframework/doma/intellij/Sql.flex new file mode 100644 index 00000000..010ecc0a --- /dev/null +++ b/src/main/java/org/domaframework/doma/intellij/Sql.flex @@ -0,0 +1,181 @@ +package org.domaframework.doma.intellij; + +import java.util.Set; +import java.util.HashSet; + +import com.intellij.lexer.FlexLexer; + +import com.intellij.psi.TokenType; +import com.intellij.psi.tree.IElementType; + +import org.domaframework.doma.intellij.psi.SqlTypes; + +%% + +%class SqlLexer +%public +%implements FlexLexer +%unicode +%function advance +%type IElementType +%{ + // SQL keywords + private static final Set KEYWORDS = Set.of( + "alter", + "all", + "and", + "asc", + "by", + "case", + "check", + "create", + "default", + "delete", + "desc", + "distinct", + "drop", + "else", + "end", + "exists", + "foreign", + "from", + "full", + "group", + "having", + "in", + "index", + "inner", + "insert", + "into", + "is", + "join", + "key", + "left", + "like", + "limit", + "not", + "null", + "offset", + "on", + "or", + "order", + "primary", + "references", + "right", + "select", + "set", + "table", + "then", + "union", + "unique", + "update", + "values", + "when", + "where" + ); + + private static boolean isKeyword(CharSequence word) { + return KEYWORDS.contains(word.toString().toLowerCase()); + } +%} + +%eof{ return; +%eof} + +// common +LineTerminator = \R +WhiteSpace = [ \n\t\f] +BlockCommentContent = ([^*]|\*+[^/])+ + +// SQL tokens +BlockCommentStart = "/*" +BlockCommentEnd = "*/" +LineComment = "--" .* {LineTerminator}? +Word = [:jletterdigit:]+ +Number = \d+ +String = \'((\'\')|[^\'])*\' + +// EL tokens +El_Number = \d+(L|(\.\d+)?[FDB])? +El_String = \"((\"\")|[^\"])*\" +El_Char = \'.\' +El_Identifier = [:jletter:][:jletterdigit:]* +El_NonWordPart = [=<>\-,/*();\R \n\t\f] + +%state EXPRESSION DIRECTIVE BLOCK_COMMENT PARSER_LEVEL_COMMENT + +%% + + { + {BlockCommentStart}/[%#\^] { yybegin(DIRECTIVE); return SqlTypes.BLOCK_COMMENT_START; } + {BlockCommentStart}/[@\"' \n\t\f[:jletter:]] { yybegin(EXPRESSION); return SqlTypes.BLOCK_COMMENT_START; } + {BlockCommentStart} { yybegin(BLOCK_COMMENT); return SqlTypes.BLOCK_COMMENT_START;} + {LineComment} { return SqlTypes.LINE_COMMENT; } + {String} { return SqlTypes.STRING; } + {Number} { return SqlTypes.NUMBER; } + {Word} { return isKeyword(yytext()) ? SqlTypes.KEYWORD : SqlTypes.WORD; } + ({LineTerminator}|{WhiteSpace})+ { return TokenType.WHITE_SPACE; } + [^] { return SqlTypes.OTHER; } +} + + { + {BlockCommentEnd} { yybegin(YYINITIAL); return SqlTypes.BLOCK_COMMENT_END; } + ":" { return SqlTypes.EL_SEPARATOR; } + "." { return SqlTypes.EL_DOT; } + "," { return SqlTypes.EL_COMMA; } + "(" { return SqlTypes.EL_LEFT_PAREN; } + ")" { return SqlTypes.EL_RIGHT_PAREN; } + "@" { return SqlTypes.EL_AT_SIGN; } + "+" { return SqlTypes.EL_PLUS;} + "-" { return SqlTypes.EL_MINUS;} + "*" { return SqlTypes.EL_ASTERISK;} + "/" { return SqlTypes.EL_SLASH;} + "%" { return SqlTypes.EL_PERCENT;} + "==" { return SqlTypes.EL_EQ;} + "!=" { return SqlTypes.EL_NE;} + "<" { return SqlTypes.EL_LT;} + "<=" { return SqlTypes.EL_LE;} + ">" { return SqlTypes.EL_GT;} + ">=" { return SqlTypes.EL_GE;} + "!" { return SqlTypes.EL_NOT;} + "&&" { return SqlTypes.EL_AND;} + "||" { return SqlTypes.EL_OR;} + "new" { return SqlTypes.EL_NEW;} + "null" { return SqlTypes.EL_NULL;} + "true" { return SqlTypes.EL_BOOLEAN;} + "false" { return SqlTypes.EL_BOOLEAN;} + {El_Number} { return SqlTypes.EL_NUMBER; } + {El_String} { return SqlTypes.EL_STRING; } + {El_Char} { return SqlTypes.EL_CHAR; } + {El_Identifier} { return SqlTypes.EL_IDENTIFIER; } + ({LineTerminator}|{WhiteSpace})+ { return TokenType.WHITE_SPACE; } + [^] { return TokenType.BAD_CHARACTER; } +} + + { + {BlockCommentEnd} { yybegin(YYINITIAL); return SqlTypes.BLOCK_COMMENT_END; } + "%if"/{El_NonWordPart} { yybegin(EXPRESSION); return SqlTypes.EL_IF; } + "%elseif"/{El_NonWordPart} { yybegin(EXPRESSION); return SqlTypes.EL_ELSEIF; } + "%else"/{El_NonWordPart} { yybegin(EXPRESSION); return SqlTypes.EL_ELSE; } + "%for"/{El_NonWordPart} { yybegin(EXPRESSION); return SqlTypes.EL_FOR; } + "%expand"/{El_NonWordPart} { yybegin(EXPRESSION); return SqlTypes.EL_EXPAND; } + "%populate"/{El_NonWordPart} { yybegin(EXPRESSION); return SqlTypes.EL_POPULATE; } + "%end"/{El_NonWordPart} { yybegin(EXPRESSION); return SqlTypes.EL_END; } + "%!" { yybegin(PARSER_LEVEL_COMMENT); return SqlTypes.EL_PARSER_LEVEL_COMMENT; } + "#" { yybegin(EXPRESSION); return SqlTypes.EL_HASH; } + "^" { yybegin(EXPRESSION); return SqlTypes.EL_CARET; } + ({LineTerminator}|{WhiteSpace})+ { return TokenType.WHITE_SPACE; } + [^] { return TokenType.BAD_CHARACTER; } +} + + { + {BlockCommentEnd} { yybegin(YYINITIAL); return SqlTypes.BLOCK_COMMENT_END; } + {BlockCommentContent} { return SqlTypes.BLOCK_COMMENT_CONTENT; } + [^] { return TokenType.BAD_CHARACTER; } +} + + { + {BlockCommentEnd} { yybegin(YYINITIAL); return SqlTypes.BLOCK_COMMENT_END; } + {BlockCommentContent} { return SqlTypes.BLOCK_COMMENT_CONTENT; } + [^] { return TokenType.BAD_CHARACTER; } +} \ No newline at end of file diff --git a/src/main/java/org/domaframework/doma/intellij/highlighter/SqlColorSettingsPage.java b/src/main/java/org/domaframework/doma/intellij/highlighter/SqlColorSettingsPage.java new file mode 100644 index 00000000..6cb57ed3 --- /dev/null +++ b/src/main/java/org/domaframework/doma/intellij/highlighter/SqlColorSettingsPage.java @@ -0,0 +1,183 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.highlighter; + +import com.intellij.icons.AllIcons.FileTypes; +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.openapi.fileTypes.SyntaxHighlighter; +import com.intellij.openapi.options.colors.AttributesDescriptor; +import com.intellij.openapi.options.colors.ColorDescriptor; +import com.intellij.openapi.options.colors.ColorSettingsPage; +import com.intellij.openapi.util.NlsContexts.ConfigurableName; +import java.util.Map; +import javax.swing.Icon; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class SqlColorSettingsPage implements ColorSettingsPage { + + private static final AttributesDescriptor[] DESCRIPTORS = + new AttributesDescriptor[] { + + // SQL Syntax + new AttributesDescriptor("SQL//Literal//String", SqlSyntaxHighlighter.STRING), + new AttributesDescriptor("SQL//Literal//Number", SqlSyntaxHighlighter.NUMBER), + new AttributesDescriptor("SQL//Syntax//Keyword", SqlSyntaxHighlighter.KEYWORD), + new AttributesDescriptor("SQL//Syntax//Other", SqlSyntaxHighlighter.OTHER), + new AttributesDescriptor("SQL//Syntax//Word", SqlSyntaxHighlighter.WORD), + new AttributesDescriptor("SQL//Comment//Line comment", SqlSyntaxHighlighter.LINE_COMMENT), + new AttributesDescriptor( + "SQL//Comment//Block content", SqlSyntaxHighlighter.BLOCK_COMMENT_CONTENT), + // Doma Syntax + new AttributesDescriptor("Expression//Identifier", SqlSyntaxHighlighter.EL_IDENTIFIER), + new AttributesDescriptor("Expression//Literal//String", SqlSyntaxHighlighter.EL_STRING), + new AttributesDescriptor("Expression//Literal//Character", SqlSyntaxHighlighter.EL_CHAR), + new AttributesDescriptor("Expression//Literal//Number", SqlSyntaxHighlighter.EL_NUMBER), + new AttributesDescriptor("Expression//Literal//Boolean", SqlSyntaxHighlighter.EL_BOOLEAN), + new AttributesDescriptor("Expression//Literal//null", SqlSyntaxHighlighter.EL_NULL), + // Directive + new AttributesDescriptor("Expression//Directive//if", SqlSyntaxHighlighter.EL_IF), + new AttributesDescriptor("Expression//Directive//for", SqlSyntaxHighlighter.EL_FOR), + new AttributesDescriptor("Expression//Directive//end", SqlSyntaxHighlighter.EL_END), + new AttributesDescriptor("Expression//Directive//elseIf", SqlSyntaxHighlighter.EL_ELSEIF), + new AttributesDescriptor("Expression//Directive//expand", SqlSyntaxHighlighter.EL_EXPAND), + new AttributesDescriptor( + "Expression//Directive//populate", SqlSyntaxHighlighter.EL_POPULATE), + // Symbol + new AttributesDescriptor("Expression//Symbol//#", SqlSyntaxHighlighter.EL_HASH), + new AttributesDescriptor("Expression//Symbol//@", SqlSyntaxHighlighter.EL_AT_SIGN), + new AttributesDescriptor("Expression//Symbol//%", SqlSyntaxHighlighter.EL_PERCENT), + new AttributesDescriptor("Expression//Symbol//^", SqlSyntaxHighlighter.EL_CARET), + new AttributesDescriptor("Expression//Symbol//!", SqlSyntaxHighlighter.EL_NOT), + new AttributesDescriptor("Expression//Symbol//:", SqlSyntaxHighlighter.EL_SEPARATOR), + new AttributesDescriptor("Expression//Symbol//.", SqlSyntaxHighlighter.EL_DOT), + new AttributesDescriptor("Expression//Symbol//,", SqlSyntaxHighlighter.EL_COMMA), + new AttributesDescriptor("Expression//Symbol//(", SqlSyntaxHighlighter.EL_LEFT_PAREN), + new AttributesDescriptor("Expression//Symbol//)", SqlSyntaxHighlighter.EL_RIGHT_PAREN), + // Operator + new AttributesDescriptor("Expression//Operator//new", SqlSyntaxHighlighter.EL_NEW), + new AttributesDescriptor("Expression//Operator//+", SqlSyntaxHighlighter.EL_PLUS), + new AttributesDescriptor("Expression//Operator//-", SqlSyntaxHighlighter.EL_MINUS), + new AttributesDescriptor("Expression//Operator//*", SqlSyntaxHighlighter.EL_ASTERISK), + new AttributesDescriptor("Expression//Operator///", SqlSyntaxHighlighter.EL_SLASH), + new AttributesDescriptor("Expression//Operator//==", SqlSyntaxHighlighter.EL_EQ), + new AttributesDescriptor("Expression//Operator//!=", SqlSyntaxHighlighter.EL_NE), + new AttributesDescriptor("Expression//Operator//>=", SqlSyntaxHighlighter.EL_GE), + new AttributesDescriptor("Expression//Operator//<=", SqlSyntaxHighlighter.EL_LE), + new AttributesDescriptor("Expression//Operator//>", SqlSyntaxHighlighter.EL_GT), + new AttributesDescriptor("Expression//Operator//&&", SqlSyntaxHighlighter.EL_AND), + new AttributesDescriptor("Expression//Operator//||", SqlSyntaxHighlighter.EL_OR), + // Comment + new AttributesDescriptor( + "Expression//Comment//Block comment start", SqlSyntaxHighlighter.BLOCK_COMMENT_START), + new AttributesDescriptor( + "Expression//Comment//Block comment end", SqlSyntaxHighlighter.BLOCK_COMMENT_END), + }; + + @Override + public @Nullable Icon getIcon() { + return FileTypes.Dtd; + } + + @Override + public @NotNull SyntaxHighlighter getHighlighter() { + return new SqlSyntaxHighlighter(); + } + + @Override + public @NonNls @NotNull String getDemoText() { + return """ + -- Sql Highlighter Demo + /** + * Set highlights as you like + */ + update product set /*%populate*/ + category = /* category */'category', + expected_sales = /* price * pieces */0, + unit_price = /* purchase / pieces */0, + /*%if mark != "XXX" */ + mark = /* mark */'mark', + /*%end */ + /*%! This comment delete */ + employee_type = ( + select /*%expand "e" */* from employee e + where + /*%for name : names */ + orderer = /* name */'orderer' + /*%if name.startWith("AA") && name.contains("_A") */ + AND div = 'A' + AND rank >= 5 + /*%elseif name.startWith("BB") || name.contains("_B") */ + AND div = 'B' + AND rank > 2 + AND rank < 5 + /*%else */ + AND div = 'C' + /*%end */ + /*%if name_has_next */ + /*# "OR" */ + /*%end */ + /*%end*/ + ) + where + type = /* @example.Type@Type.getValue() */'type' + and cost_limit <= /* cost + 100 */0 + and cost_limit >= /* cost - 100 */99999; + + -- Demo Text2 + select p.project_id + , p.project_name + , p.project_type + , p.project_category + , p.pre_project + from project p + where p.project_type = /* new Project().type */'type' + and( + /*%for project : preProjects */ + /*%if project.category != null */ + project_category = /* project.category.plus('t') */'category' + /*%elseif project.pre == true */ + pre_project = /* project.preProjectId */0 + /*%if project_has_next */ + /*# "OR" */ + /*%end */ + /*%end */ + /*%end */ + ) + """; + } + + @Override + public @Nullable Map getAdditionalHighlightingTagToDescriptorMap() { + return Map.of(); + } + + @Override + public AttributesDescriptor @NotNull [] getAttributeDescriptors() { + return DESCRIPTORS; + } + + @Override + public ColorDescriptor @NotNull [] getColorDescriptors() { + return ColorDescriptor.EMPTY_ARRAY; + } + + @Override + public @NotNull @ConfigurableName String getDisplayName() { + return "SQL(Doma Tools)"; + } +} diff --git a/src/main/java/org/domaframework/doma/intellij/highlighter/SqlSyntaxHighlighter.java b/src/main/java/org/domaframework/doma/intellij/highlighter/SqlSyntaxHighlighter.java new file mode 100644 index 00000000..7d842966 --- /dev/null +++ b/src/main/java/org/domaframework/doma/intellij/highlighter/SqlSyntaxHighlighter.java @@ -0,0 +1,203 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.highlighter; + +import static com.intellij.openapi.editor.colors.TextAttributesKey.createTextAttributesKey; + +import com.intellij.lexer.Lexer; +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors; +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.openapi.fileTypes.SyntaxHighlighterBase; +import com.intellij.psi.tree.IElementType; +import java.util.HashMap; +import java.util.Map; +import org.domaframework.doma.intellij.psi.SqlTypes; +import org.domaframework.doma.intellij.setting.SqlLexerAdapter; +import org.jetbrains.annotations.NotNull; + +public class SqlSyntaxHighlighter extends SyntaxHighlighterBase { + + private static final Map map = new HashMap<>(64); + + public static final TextAttributesKey KEYWORD = + createTextAttributesKey("SQL_KEYWORD", DefaultLanguageHighlighterColors.METADATA); + public static final TextAttributesKey WORD = + createTextAttributesKey("SQL_WORD", DefaultLanguageHighlighterColors.IDENTIFIER); + public static final TextAttributesKey STRING = + createTextAttributesKey("SQL_STRING", DefaultLanguageHighlighterColors.STRING); + public static final TextAttributesKey NUMBER = + createTextAttributesKey("SQL_NUMBER", DefaultLanguageHighlighterColors.NUMBER); + public static final TextAttributesKey OTHER = + createTextAttributesKey("SQL_OTHER", DefaultLanguageHighlighterColors.IDENTIFIER); + public static final TextAttributesKey BLOCK_COMMENT_START = + createTextAttributesKey( + "SQL_BLOCK_COMMENT_START", DefaultLanguageHighlighterColors.BLOCK_COMMENT); + public static final TextAttributesKey BLOCK_COMMENT_END = + createTextAttributesKey( + "SQL_BLOCK_COMMENT_END", DefaultLanguageHighlighterColors.BLOCK_COMMENT); + public static final TextAttributesKey BLOCK_COMMENT_CONTENT = + createTextAttributesKey( + "SQL_BLOCK_COMMENT_CONTENT", DefaultLanguageHighlighterColors.BLOCK_COMMENT); + public static final TextAttributesKey LINE_COMMENT = + createTextAttributesKey("SQL_LINE_COMMENT", DefaultLanguageHighlighterColors.LINE_COMMENT); + + public static final TextAttributesKey EL_IDENTIFIER = + createTextAttributesKey( + "EL_IDENTIFIER", DefaultLanguageHighlighterColors.FUNCTION_DECLARATION); + public static final TextAttributesKey EL_IF = + createTextAttributesKey("EL_IF", DefaultLanguageHighlighterColors.KEYWORD); + public static final TextAttributesKey EL_ELSEIF = + createTextAttributesKey("EL_ELSEIF", DefaultLanguageHighlighterColors.KEYWORD); + public static final TextAttributesKey EL_ELSE = + createTextAttributesKey("EL_ELSE", DefaultLanguageHighlighterColors.KEYWORD); + public static final TextAttributesKey EL_FOR = + createTextAttributesKey("EL_FOR", DefaultLanguageHighlighterColors.KEYWORD); + public static final TextAttributesKey EL_EXPAND = + createTextAttributesKey("EL_EXPAND", DefaultLanguageHighlighterColors.KEYWORD); + public static final TextAttributesKey EL_POPULATE = + createTextAttributesKey("EL_POPULATE", DefaultLanguageHighlighterColors.KEYWORD); + public static final TextAttributesKey EL_PARSER_LEVEL_COMMENT = + createTextAttributesKey("EL_PARSER_LEVEL_COMMENT", DefaultLanguageHighlighterColors.KEYWORD); + public static final TextAttributesKey EL_END = + createTextAttributesKey("EL_END", DefaultLanguageHighlighterColors.KEYWORD); + public static final TextAttributesKey EL_HASH = + createTextAttributesKey("EL_HASH", DefaultLanguageHighlighterColors.KEYWORD); + public static final TextAttributesKey EL_CARET = + createTextAttributesKey("EL_CARET", DefaultLanguageHighlighterColors.KEYWORD); + + public static final TextAttributesKey EL_DOT = + createTextAttributesKey("EL_DOT", DefaultLanguageHighlighterColors.DOT); + public static final TextAttributesKey EL_COMMA = + createTextAttributesKey("EL_COMMA", DefaultLanguageHighlighterColors.COMMA); + public static final TextAttributesKey EL_AT_SIGN = + createTextAttributesKey("EL_AT_SIGN", DefaultLanguageHighlighterColors.KEYWORD); + public static final TextAttributesKey EL_LEFT_PAREN = + createTextAttributesKey("EL_LEFT_PAREN", DefaultLanguageHighlighterColors.PARENTHESES); + public static final TextAttributesKey EL_RIGHT_PAREN = + createTextAttributesKey("EL_RIGHT_PAREN", DefaultLanguageHighlighterColors.PARENTHESES); + + public static final TextAttributesKey EL_PLUS = + createTextAttributesKey("EL_PLUS", DefaultLanguageHighlighterColors.OPERATION_SIGN); + public static final TextAttributesKey EL_MINUS = + createTextAttributesKey("EL_MINUS", DefaultLanguageHighlighterColors.OPERATION_SIGN); + public static final TextAttributesKey EL_ASTERISK = + createTextAttributesKey("EL_ASTERISK", DefaultLanguageHighlighterColors.OPERATION_SIGN); + public static final TextAttributesKey EL_SLASH = + createTextAttributesKey("EL_SLASH", DefaultLanguageHighlighterColors.OPERATION_SIGN); + public static final TextAttributesKey EL_PERCENT = + createTextAttributesKey("EL_PERCENT", DefaultLanguageHighlighterColors.OPERATION_SIGN); + public static final TextAttributesKey EL_SEPARATOR = + createTextAttributesKey("EL_SEPARATOR", DefaultLanguageHighlighterColors.OPERATION_SIGN); + + public static final TextAttributesKey EL_EQ = + createTextAttributesKey("EL_EQ", DefaultLanguageHighlighterColors.OPERATION_SIGN); + public static final TextAttributesKey EL_NE = + createTextAttributesKey("EL_NE", DefaultLanguageHighlighterColors.OPERATION_SIGN); + public static final TextAttributesKey EL_LT = + createTextAttributesKey("EL_LT", DefaultLanguageHighlighterColors.OPERATION_SIGN); + public static final TextAttributesKey EL_LE = + createTextAttributesKey("EL_LE", DefaultLanguageHighlighterColors.OPERATION_SIGN); + public static final TextAttributesKey EL_GT = + createTextAttributesKey("EL_GT", DefaultLanguageHighlighterColors.OPERATION_SIGN); + public static final TextAttributesKey EL_GE = + createTextAttributesKey("EL_GE", DefaultLanguageHighlighterColors.OPERATION_SIGN); + public static final TextAttributesKey EL_NOT = + createTextAttributesKey("EL_NOT", DefaultLanguageHighlighterColors.OPERATION_SIGN); + public static final TextAttributesKey EL_AND = + createTextAttributesKey("EL_AND", DefaultLanguageHighlighterColors.OPERATION_SIGN); + public static final TextAttributesKey EL_OR = + createTextAttributesKey("EL_OR", DefaultLanguageHighlighterColors.OPERATION_SIGN); + + public static final TextAttributesKey EL_NEW = + createTextAttributesKey("EL_NEW", DefaultLanguageHighlighterColors.KEYWORD); + + public static final TextAttributesKey EL_NULL = + createTextAttributesKey("EL_NULL", DefaultLanguageHighlighterColors.CONSTANT); + public static final TextAttributesKey EL_BOOLEAN = + createTextAttributesKey("EL_BOOLEAN", DefaultLanguageHighlighterColors.CONSTANT); + public static final TextAttributesKey EL_STRING = + createTextAttributesKey("EL_STRING", DefaultLanguageHighlighterColors.STRING); + public static final TextAttributesKey EL_CHAR = + createTextAttributesKey("EL_CHAR", DefaultLanguageHighlighterColors.STRING); + public static final TextAttributesKey EL_NUMBER = + createTextAttributesKey("EL_LONG", DefaultLanguageHighlighterColors.NUMBER); + + static { + map.put(SqlTypes.KEYWORD, KEYWORD); + map.put(SqlTypes.STRING, STRING); + map.put(SqlTypes.OTHER, OTHER); + map.put(SqlTypes.WORD, WORD); + map.put(SqlTypes.NUMBER, NUMBER); + map.put(SqlTypes.BLOCK_COMMENT_START, BLOCK_COMMENT_START); + map.put(SqlTypes.BLOCK_COMMENT_CONTENT, BLOCK_COMMENT_CONTENT); + map.put(SqlTypes.BLOCK_COMMENT_END, BLOCK_COMMENT_END); + map.put(SqlTypes.LINE_COMMENT, LINE_COMMENT); + + map.put(SqlTypes.EL_IDENTIFIER, EL_IDENTIFIER); + map.put(SqlTypes.EL_IF, EL_IF); + map.put(SqlTypes.EL_ELSEIF, EL_ELSEIF); + map.put(SqlTypes.EL_ELSE, EL_ELSE); + map.put(SqlTypes.EL_EXPAND, EL_EXPAND); + map.put(SqlTypes.EL_POPULATE, EL_POPULATE); + map.put(SqlTypes.EL_PARSER_LEVEL_COMMENT, EL_PARSER_LEVEL_COMMENT); + map.put(SqlTypes.EL_FOR, EL_FOR); + map.put(SqlTypes.EL_END, EL_END); + map.put(SqlTypes.EL_HASH, EL_HASH); + map.put(SqlTypes.EL_CARET, EL_CARET); + + map.put(SqlTypes.EL_DOT, EL_DOT); + map.put(SqlTypes.EL_COMMA, EL_COMMA); + map.put(SqlTypes.EL_AT_SIGN, EL_AT_SIGN); + map.put(SqlTypes.EL_LEFT_PAREN, EL_LEFT_PAREN); + map.put(SqlTypes.EL_RIGHT_PAREN, EL_RIGHT_PAREN); + + map.put(SqlTypes.EL_PLUS, EL_PLUS); + map.put(SqlTypes.EL_MINUS, EL_MINUS); + map.put(SqlTypes.EL_ASTERISK, EL_ASTERISK); + map.put(SqlTypes.EL_SLASH, EL_SLASH); + map.put(SqlTypes.EL_PERCENT, EL_PERCENT); + + map.put(SqlTypes.EL_EQ, EL_EQ); + map.put(SqlTypes.EL_NE, EL_NE); + map.put(SqlTypes.EL_LT, EL_LT); + map.put(SqlTypes.EL_LE, EL_LE); + map.put(SqlTypes.EL_GT, EL_GT); + map.put(SqlTypes.EL_GE, EL_GE); + map.put(SqlTypes.EL_NOT, EL_NOT); + map.put(SqlTypes.EL_AND, EL_AND); + map.put(SqlTypes.EL_OR, EL_OR); + map.put(SqlTypes.EL_SEPARATOR, EL_SEPARATOR); + + map.put(SqlTypes.EL_NEW, EL_NEW); + + map.put(SqlTypes.EL_NULL, EL_NULL); + map.put(SqlTypes.EL_BOOLEAN, EL_BOOLEAN); + map.put(SqlTypes.EL_STRING, EL_STRING); + map.put(SqlTypes.EL_CHAR, EL_CHAR); + map.put(SqlTypes.EL_NUMBER, EL_NUMBER); + } + + @NotNull + @Override + public Lexer getHighlightingLexer() { + return new SqlLexerAdapter(); + } + + @Override + public TextAttributesKey @NotNull [] getTokenHighlights(IElementType tokenType) { + return pack(map.get(tokenType)); + } +} diff --git a/src/main/java/org/domaframework/doma/intellij/highlighter/SqlSyntaxHighlighterFactory.java b/src/main/java/org/domaframework/doma/intellij/highlighter/SqlSyntaxHighlighterFactory.java new file mode 100644 index 00000000..ae94445a --- /dev/null +++ b/src/main/java/org/domaframework/doma/intellij/highlighter/SqlSyntaxHighlighterFactory.java @@ -0,0 +1,31 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.highlighter; + +import com.intellij.openapi.fileTypes.SyntaxHighlighter; +import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +final class SqlSyntaxHighlighterFactory extends SyntaxHighlighterFactory { + @Override + public @NotNull SyntaxHighlighter getSyntaxHighlighter( + @Nullable Project project, @Nullable VirtualFile virtualFile) { + return new SqlSyntaxHighlighter(); + } +} diff --git a/src/main/java/org/domaframework/doma/intellij/psi/SqlCustomElCommentExpr.java b/src/main/java/org/domaframework/doma/intellij/psi/SqlCustomElCommentExpr.java new file mode 100644 index 00000000..c0825a0c --- /dev/null +++ b/src/main/java/org/domaframework/doma/intellij/psi/SqlCustomElCommentExpr.java @@ -0,0 +1,20 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.psi; + +import com.intellij.psi.PsiComment; + +public interface SqlCustomElCommentExpr extends PsiComment {} diff --git a/src/main/java/org/domaframework/doma/intellij/psi/SqlElCommentExprImpl.java b/src/main/java/org/domaframework/doma/intellij/psi/SqlElCommentExprImpl.java new file mode 100644 index 00000000..738f7350 --- /dev/null +++ b/src/main/java/org/domaframework/doma/intellij/psi/SqlElCommentExprImpl.java @@ -0,0 +1,63 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.psi; + +import com.intellij.extapi.psi.ASTWrapperPsiElement; +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; + +/** + * Represents an implementation of a SQL EL (Expression Language) comment expression in the PSI + * (Program Structure Interface). This implementation serves as a base class for more specialized EL + * comment expressions and inherits from ASTWrapperPsiElement. It also implements + * SqlCustomElCommentExpr, which designates it as a PSI comment in the PSI tree. + * + *

The main purpose of this class is to provide methods to interact with the underlying AST node, + * define its token type, and delegate visitor-specific behavior for handling SQL EL comment + * expressions. + * + *

Key Responsibilities: - Retrieve the underlying AST node associated with this PSI element. - + * Identify the element type of the token in the AST. - Implement visitor pattern methods for + * processing SQL expressions and comments. + */ +public class SqlElCommentExprImpl extends ASTWrapperPsiElement implements SqlCustomElCommentExpr { + + public SqlElCommentExprImpl(@NotNull ASTNode node) { + super(node); + } + + @Override + public @NotNull ASTNode getNode() { + return super.getNode(); + } + + @Override + public @NotNull IElementType getTokenType() { + return getNode().getElementType(); + } + + public void accept(@NotNull SqlVisitor visitor) { + visitor.visitElExpr((SqlElExpr) this); + } + + @Override + public void accept(@NotNull PsiElementVisitor visitor) { + if (visitor instanceof SqlVisitor) accept((SqlVisitor) visitor); + else super.accept(visitor); + } +} diff --git a/src/main/java/org/domaframework/doma/intellij/psi/SqlElementType.java b/src/main/java/org/domaframework/doma/intellij/psi/SqlElementType.java new file mode 100644 index 00000000..accbdd29 --- /dev/null +++ b/src/main/java/org/domaframework/doma/intellij/psi/SqlElementType.java @@ -0,0 +1,25 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.psi; + +import com.intellij.psi.tree.IElementType; +import org.domaframework.doma.intellij.setting.SqlLanguage; + +public class SqlElementType extends IElementType { + public SqlElementType(String debugName) { + super(debugName, SqlLanguage.INSTANCE); + } +} diff --git a/src/main/java/org/domaframework/doma/intellij/psi/SqlFile.java b/src/main/java/org/domaframework/doma/intellij/psi/SqlFile.java new file mode 100644 index 00000000..d53b5d7b --- /dev/null +++ b/src/main/java/org/domaframework/doma/intellij/psi/SqlFile.java @@ -0,0 +1,39 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.psi; + +import com.intellij.extapi.psi.PsiFileBase; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.psi.FileViewProvider; +import org.domaframework.doma.intellij.setting.SqlFileType; +import org.domaframework.doma.intellij.setting.SqlLanguage; +import org.jetbrains.annotations.NotNull; + +public class SqlFile extends PsiFileBase { + public SqlFile(@NotNull FileViewProvider viewProvider) { + super(viewProvider, SqlLanguage.INSTANCE); + } + + @Override + public @NotNull FileType getFileType() { + return SqlFileType.INSTANCE; + } + + @Override + public String toString() { + return "SQL File"; + } +} diff --git a/src/main/java/org/domaframework/doma/intellij/psi/SqlTokenSets.java b/src/main/java/org/domaframework/doma/intellij/psi/SqlTokenSets.java new file mode 100644 index 00000000..c40c10f6 --- /dev/null +++ b/src/main/java/org/domaframework/doma/intellij/psi/SqlTokenSets.java @@ -0,0 +1,22 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.psi; + +import com.intellij.psi.tree.TokenSet; + +public interface SqlTokenSets { + TokenSet COMMENTS = TokenSet.create(SqlTypes.BLOCK_COMMENT_CONTENT); +} diff --git a/src/main/java/org/domaframework/doma/intellij/psi/SqlTokenType.java b/src/main/java/org/domaframework/doma/intellij/psi/SqlTokenType.java new file mode 100644 index 00000000..072e24ef --- /dev/null +++ b/src/main/java/org/domaframework/doma/intellij/psi/SqlTokenType.java @@ -0,0 +1,30 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.psi; + +import com.intellij.psi.tree.IElementType; +import org.domaframework.doma.intellij.setting.SqlLanguage; + +public class SqlTokenType extends IElementType { + public SqlTokenType(String debugName) { + super(debugName, SqlLanguage.INSTANCE); + } + + @Override + public String toString() { + return "SqlTokenType." + super.toString(); + } +} diff --git a/src/main/java/org/domaframework/doma/intellij/setting/SqlFileType.java b/src/main/java/org/domaframework/doma/intellij/setting/SqlFileType.java new file mode 100644 index 00000000..324c180f --- /dev/null +++ b/src/main/java/org/domaframework/doma/intellij/setting/SqlFileType.java @@ -0,0 +1,52 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.setting; + +import com.intellij.openapi.fileTypes.LanguageFileType; +import javax.swing.Icon; +import org.jetbrains.annotations.NotNull; + +public final class SqlFileType extends LanguageFileType { + + public static final SqlFileType INSTANCE = new SqlFileType(); + + private SqlFileType() { + super(SqlLanguage.INSTANCE); + } + + @NotNull + @Override + public String getName() { + return "DomaSql"; + } + + @NotNull + @Override + public String getDescription() { + return "Doma sql template"; + } + + @NotNull + @Override + public String getDefaultExtension() { + return "sql"; + } + + @Override + public Icon getIcon() { + return SqlIcon.FILE; + } +} diff --git a/src/main/java/org/domaframework/doma/intellij/setting/SqlIcon.java b/src/main/java/org/domaframework/doma/intellij/setting/SqlIcon.java new file mode 100644 index 00000000..db43f1b2 --- /dev/null +++ b/src/main/java/org/domaframework/doma/intellij/setting/SqlIcon.java @@ -0,0 +1,23 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.setting; + +import com.intellij.openapi.util.IconLoader; +import javax.swing.Icon; + +public class SqlIcon { + public static final Icon FILE = IconLoader.getIcon("/icons/SqlFile.svg", SqlFileType.class); +} diff --git a/src/main/java/org/domaframework/doma/intellij/setting/SqlLanguage.java b/src/main/java/org/domaframework/doma/intellij/setting/SqlLanguage.java new file mode 100644 index 00000000..5042aa1b --- /dev/null +++ b/src/main/java/org/domaframework/doma/intellij/setting/SqlLanguage.java @@ -0,0 +1,27 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.setting; + +import com.intellij.lang.Language; + +public class SqlLanguage extends Language { + + public static final SqlLanguage INSTANCE = new SqlLanguage(); + + private SqlLanguage() { + super("DomaSql"); + } +} diff --git a/src/main/java/org/domaframework/doma/intellij/setting/SqlLexerAdapter.java b/src/main/java/org/domaframework/doma/intellij/setting/SqlLexerAdapter.java new file mode 100644 index 00000000..6d6e1d56 --- /dev/null +++ b/src/main/java/org/domaframework/doma/intellij/setting/SqlLexerAdapter.java @@ -0,0 +1,25 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.setting; + +import com.intellij.lexer.FlexAdapter; +import org.domaframework.doma.intellij.SqlLexer; + +public class SqlLexerAdapter extends FlexAdapter { + public SqlLexerAdapter() { + super(new SqlLexer(null)); + } +} diff --git a/src/main/java/org/domaframework/doma/intellij/setting/SqlParserDefinition.java b/src/main/java/org/domaframework/doma/intellij/setting/SqlParserDefinition.java new file mode 100644 index 00000000..fa80cdc1 --- /dev/null +++ b/src/main/java/org/domaframework/doma/intellij/setting/SqlParserDefinition.java @@ -0,0 +1,71 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.setting; + +import com.intellij.lang.ASTNode; +import com.intellij.lang.ParserDefinition; +import com.intellij.lang.PsiParser; +import com.intellij.lexer.Lexer; +import com.intellij.openapi.project.Project; +import com.intellij.psi.FileViewProvider; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.tree.IFileElementType; +import com.intellij.psi.tree.TokenSet; +import org.domaframework.doma.intellij.SqlParser; +import org.domaframework.doma.intellij.psi.SqlFile; +import org.domaframework.doma.intellij.psi.SqlTokenSets; +import org.domaframework.doma.intellij.psi.SqlTypes; +import org.jetbrains.annotations.NotNull; + +public final class SqlParserDefinition implements ParserDefinition { + public static final IFileElementType FILE = new IFileElementType(SqlLanguage.INSTANCE); + + @Override + public @NotNull Lexer createLexer(Project project) { + return new SqlLexerAdapter(); + } + + @Override + public @NotNull PsiParser createParser(Project project) { + return new SqlParser(); + } + + @Override + public @NotNull IFileElementType getFileNodeType() { + return FILE; + } + + @Override + public @NotNull TokenSet getCommentTokens() { + return SqlTokenSets.COMMENTS; + } + + @Override + public @NotNull TokenSet getStringLiteralElements() { + return TokenSet.EMPTY; + } + + @Override + public @NotNull PsiElement createElement(ASTNode astNode) { + return SqlTypes.Factory.createElement(astNode); + } + + @Override + public @NotNull PsiFile createFile(@NotNull FileViewProvider fileViewProvider) { + return new SqlFile(fileViewProvider); + } +} diff --git a/src/main/java/org/domaframework/doma/intellij/setting/SqlParserUtil.java b/src/main/java/org/domaframework/doma/intellij/setting/SqlParserUtil.java new file mode 100644 index 00000000..3d69a4aa --- /dev/null +++ b/src/main/java/org/domaframework/doma/intellij/setting/SqlParserUtil.java @@ -0,0 +1,20 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.setting; + +import com.intellij.lang.parser.GeneratedParserUtilBase; + +public class SqlParserUtil extends GeneratedParserUtilBase {} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/action/dao/GenerateSqlAction.kt b/src/main/kotlin/org/domaframework/doma/intellij/action/dao/GenerateSqlAction.kt new file mode 100644 index 00000000..6ee6e95b --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/action/dao/GenerateSqlAction.kt @@ -0,0 +1,75 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.action.dao + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiMethod +import com.intellij.psi.util.PsiTreeUtil +import org.domaframework.doma.intellij.common.PluginLoggerUtil +import org.domaframework.doma.intellij.common.dao.getDaoClass +import org.domaframework.doma.intellij.common.isJavaOrKotlinFileType +import org.domaframework.doma.intellij.common.psi.PsiDaoMethod + +/** + * Action class that generates SQL from Dao method + */ +class GenerateSqlAction : AnAction() { + private var currentFile: PsiFile? = null + + override fun update(e: AnActionEvent) { + e.presentation.isEnabledAndVisible = false + currentFile = e.getData(CommonDataKeys.PSI_FILE) ?: return + val editor = e.getData(CommonDataKeys.EDITOR) ?: return + getDaoClass(currentFile!!) ?: return + val element = currentFile!!.findElementAt(editor.caretModel.offset) ?: return + val method = PsiTreeUtil.getParentOfType(element, PsiMethod::class.java) ?: return + + val psiDaoMethod = PsiDaoMethod(e.project!!, method) + e.presentation.isEnabledAndVisible = + psiDaoMethod.isUseSqlFileMethod() && + isJavaOrKotlinFileType(currentFile!!) && + psiDaoMethod.sqlFile == null + } + + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT + + override fun actionPerformed(e: AnActionEvent) { + val startTime = System.nanoTime() + val inputEvent = e.inputEvent + PluginLoggerUtil.countLoggingByAction( + this::class.java.simpleName, + "CallGenerateSql", + inputEvent, + startTime, + ) + val project = e.project ?: return + val editor = e.getData(CommonDataKeys.EDITOR) ?: return + val element = currentFile!!.findElementAt(editor.caretModel.offset) ?: return + val method = PsiTreeUtil.getParentOfType(element, PsiMethod::class.java) ?: return + val psiDaoMethod = PsiDaoMethod(project, method) + psiDaoMethod.generateSqlFile() + PluginLoggerUtil.countLoggingByAction( + this::class.java.simpleName, + "GenerateSql", + inputEvent, + startTime, + ) + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/action/dao/JumpToSQLFromDaoAction.kt b/src/main/kotlin/org/domaframework/doma/intellij/action/dao/JumpToSQLFromDaoAction.kt new file mode 100644 index 00000000..7a6b19c2 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/action/dao/JumpToSQLFromDaoAction.kt @@ -0,0 +1,77 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.action.dao + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiMethod +import com.intellij.psi.util.PsiTreeUtil +import org.domaframework.doma.intellij.common.PluginLoggerUtil +import org.domaframework.doma.intellij.common.dao.getDaoClass +import org.domaframework.doma.intellij.common.dao.jumpSqlFromDao +import org.domaframework.doma.intellij.common.isJavaOrKotlinFileType +import org.domaframework.doma.intellij.common.psi.PsiDaoMethod + +class JumpToSQLFromDaoAction : AnAction() { + private var currentFile: PsiFile? = null + + override fun update(e: AnActionEvent) { + e.presentation.isEnabledAndVisible = false + currentFile = e.getData(CommonDataKeys.PSI_FILE) ?: return + + if (!isJavaOrKotlinFileType(currentFile!!)) return + getDaoClass(currentFile!!) ?: return + + val editor = e.getData(CommonDataKeys.EDITOR) ?: return + val element = currentFile!!.findElementAt(editor.caretModel.offset) ?: return + val method = PsiTreeUtil.getParentOfType(element, PsiMethod::class.java) ?: return + val psiDaoMethod = PsiDaoMethod(e.project!!, method) + + e.presentation.isEnabledAndVisible = + psiDaoMethod.isUseSqlFileMethod() && + psiDaoMethod.sqlFile != null + } + + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT + + override fun actionPerformed(e: AnActionEvent) { + val startTime = System.nanoTime() + val inputEvent = e.inputEvent + PluginLoggerUtil.countLoggingByAction( + this::class.java.simpleName, + "CallJumpToSql", + inputEvent, + startTime, + ) + val editor = e.getData(CommonDataKeys.EDITOR) ?: return + val element = currentFile!!.findElementAt(editor.caretModel.offset) ?: return + val method = PsiTreeUtil.getParentOfType(element, PsiMethod::class.java) ?: return + val project = e.project ?: return + + val psiDaoMethod = PsiDaoMethod(project, method) + + PluginLoggerUtil.countLoggingByAction( + this::class.java.simpleName, + "JumpToSql", + inputEvent, + startTime, + ) + jumpSqlFromDao(project, psiDaoMethod.sqlFile!!) + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/action/sql/BindVariableElement.kt b/src/main/kotlin/org/domaframework/doma/intellij/action/sql/BindVariableElement.kt new file mode 100644 index 00000000..9c682c5e --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/action/sql/BindVariableElement.kt @@ -0,0 +1,34 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.action.sql + +import com.intellij.psi.PsiElement +import com.intellij.util.PsiNavigateUtil + +/** + * Jump from bind variable + */ +class BindVariableElement( + private val bindProperty: PsiElement, +) { + fun jumpToEntity() { + PsiNavigateUtil.navigate(bindProperty) + } + + fun jumpToDao() { + PsiNavigateUtil.navigate(bindProperty) + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/action/sql/JumpToDaoFromSQLAction.kt b/src/main/kotlin/org/domaframework/doma/intellij/action/sql/JumpToDaoFromSQLAction.kt new file mode 100644 index 00000000..f331f7dc --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/action/sql/JumpToDaoFromSQLAction.kt @@ -0,0 +1,66 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.action.sql + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.psi.PsiFile +import org.domaframework.doma.intellij.common.PluginLoggerUtil +import org.domaframework.doma.intellij.common.dao.findDaoFile +import org.domaframework.doma.intellij.common.dao.findDaoMethod +import org.domaframework.doma.intellij.common.dao.jumpToDaoMethod +import org.domaframework.doma.intellij.common.isSupportFileType + +/*** + * Action to jump from SQL file to corresponding Dao function + */ +class JumpToDaoFromSQLAction : AnAction() { + private var currentFile: PsiFile? = null + + override fun update(e: AnActionEvent) { + e.presentation.isEnabledAndVisible = false + currentFile = e.getData(CommonDataKeys.PSI_FILE) ?: return + findDaoMethod(currentFile!!) ?: return + e.presentation.isEnabledAndVisible = + isSupportFileType(currentFile!!.virtualFile) + } + + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT + + override fun actionPerformed(e: AnActionEvent) { + val startTime = System.nanoTime() + val inputEvent = e.inputEvent + PluginLoggerUtil.countLoggingByAction( + this::class.java.simpleName, + "CallJumpToDao", + inputEvent, + startTime, + ) + + val project = e.project ?: return + val daoFile = findDaoFile(project, currentFile?.virtualFile!!) ?: return + + jumpToDaoMethod(project, currentFile?.virtualFile?.nameWithoutExtension!!, daoFile) + PluginLoggerUtil.countLoggingByAction( + this::class.java.simpleName, + "JumpToDao", + inputEvent, + startTime, + ) + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/action/sql/JumpToDeclarationFromSqlAction.kt b/src/main/kotlin/org/domaframework/doma/intellij/action/sql/JumpToDeclarationFromSqlAction.kt new file mode 100644 index 00000000..78683f27 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/action/sql/JumpToDeclarationFromSqlAction.kt @@ -0,0 +1,278 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.action.sql + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiType +import com.intellij.psi.tree.IFileElementType +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.PsiTypesUtil +import com.intellij.psi.util.elementType +import org.domaframework.doma.intellij.common.PluginLoggerUtil +import org.domaframework.doma.intellij.common.dao.findDaoMethod +import org.domaframework.doma.intellij.common.isJavaOrKotlinFileType +import org.domaframework.doma.intellij.common.psi.PsiParentClass +import org.domaframework.doma.intellij.common.psi.PsiStaticElement +import org.domaframework.doma.intellij.extension.psi.findParameter +import org.domaframework.doma.intellij.extension.psi.getDomaAnnotationType +import org.domaframework.doma.intellij.extension.psi.getIterableClazz +import org.domaframework.doma.intellij.extension.psi.initPsiFileAndElement +import org.domaframework.doma.intellij.extension.psi.isNotWhiteSpace +import org.domaframework.doma.intellij.extension.psi.methodParameters +import org.domaframework.doma.intellij.psi.SqlElClass +import org.domaframework.doma.intellij.psi.SqlElPrimaryExpr +import org.domaframework.doma.intellij.psi.SqlElStaticFieldAccessExpr +import org.domaframework.doma.intellij.psi.SqlTypes + +/** + * Action to jump from SQL bind variable to Dao method argument and Entity class + */ +class JumpToDeclarationFromSqlAction : AnAction() { + private var element: PsiElement? = null + private var currentFile: PsiFile? = null + private var logActionFileType = "Sql" + + override fun update(e: AnActionEvent) { + e.presentation.isEnabledAndVisible = false + val editor = e.getData(CommonDataKeys.EDITOR) ?: return + val caretOffset = editor.caretModel.offset + + currentFile = e.getData(CommonDataKeys.PSI_FILE) ?: return + currentFile?.let { element = it.findElementAt(caretOffset) ?: return } + if (element == null) return + + val project = element?.project ?: return + if (isJavaOrKotlinFileType(currentFile ?: return)) { + currentFile = currentFile!!.initPsiFileAndElement(project, caretOffset) + } + findDaoMethod(currentFile ?: return) ?: return + + val staticDirection = element?.parent + val staticDirective = getStaticDirective(staticDirection, element!!.text) + if (staticDirective != null) { + e.presentation.isEnabledAndVisible = true + return + } + + val targetElement = getBlockCommentElements(element ?: return) + if (targetElement.isNotEmpty()) e.presentation.isEnabledAndVisible = true + } + + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT + + override fun actionPerformed(e: AnActionEvent) { + val startTime = System.nanoTime() + val staticDirection = element?.parent + val staticDirective = getStaticDirective(staticDirection, element!!.text) + if (staticDirective != null) { + BindVariableElement(staticDirective).jumpToEntity() + PluginLoggerUtil.countLoggingByAction( + this::class.java.simpleName, + "JumpToStaticBy$logActionFileType", + e.inputEvent, + startTime, + ) + return + } + if (element == null) return + // TODO Since the update also checks whether the action is to be executed, + // delete it if it is unnecessary. + if (isNotBindVariable(element!!)) return + + val targetElement = getBlockCommentElements(element!!) + if (targetElement.isEmpty()) return + + val daoMethod = currentFile?.let { file -> findDaoMethod(file) } ?: return + when (element!!.textOffset) { + targetElement.first().textOffset -> + jumpToDaoMethodParameter( + daoMethod, + element!!, + logActionFileType, + e, + startTime, + ) + + else -> jumpToEntity(daoMethod, targetElement, logActionFileType, e, startTime) + } + } + + private fun getStaticDirective( + staticDirection: PsiElement?, + elementName: String, + ): PsiElement? { + if (staticDirection == null) return null + + // Jump to class definition + if (staticDirection is SqlElClass) { + val psiStaticElement = PsiStaticElement(staticDirection.text, currentFile!!) + return psiStaticElement.getRefClazz() + } + + // Jump from field or method to definition (assuming the top element is static) + val staticAccessParent = staticDirection.parent + if (staticDirection is SqlElStaticFieldAccessExpr || + staticAccessParent is SqlElStaticFieldAccessExpr + ) { + val psiStaticElement = + PsiStaticElement( + staticAccessParent.children + .firstOrNull() + ?.text ?: "", + currentFile!!, + ) + val javaClazz = psiStaticElement.getRefClazz() ?: return null + val psiParentClass = PsiParentClass(PsiTypesUtil.getClassType(javaClazz)) + psiParentClass.findField(elementName)?.let { + return it + } + psiParentClass.findMethod(elementName)?.let { + return it + } + } + return null + } + + /** + * If you execute an action from a blank space, + * it will trace back to just below the drive, so play it. + */ + private fun isNotBindVariable(it: PsiElement) = + ( + it.parent.elementType is IFileElementType && + it.elementType != SqlTypes.EL_IDENTIFIER && + it !is SqlElPrimaryExpr && + !it.isNotWhiteSpace() + ) + + private fun jumpToDaoMethodParameter( + daoMethod: PsiMethod, + it: PsiElement, + logActionFileType: String, + e: AnActionEvent, + startTime: Long, + ) { + daoMethod + .let { method -> + method.methodParameters.firstOrNull { param -> + param.name == it.text + } + }?.originalElement + ?.let { originalElm -> + BindVariableElement(originalElm).jumpToDao() + PluginLoggerUtil.countLoggingByAction( + this::class.java.simpleName, + "JumpToDaoMethodParameterBy$logActionFileType", + e.inputEvent, + startTime, + ) + return + } + } + + private fun jumpToEntity( + daoMethod: PsiMethod, + targetElement: List, + logActionFileType: String, + e: AnActionEvent, + startTime: Long, + ) { + val topParam = daoMethod.findParameter(targetElement.first().text) ?: return + val parentClass = topParam.getIterableClazz(daoMethod.getDomaAnnotationType()) + val bindEntity = + getBindProperty( + targetElement.toList(), + parentClass, + ) + bindEntity?.jumpToEntity() + PluginLoggerUtil.countLoggingByAction( + this::class.java.simpleName, + "JumpToBindVariableBy$logActionFileType", + e.inputEvent, + startTime, + ) + } + + private fun getBlockCommentElements(element: PsiElement): List { + val nodeElm = + PsiTreeUtil + .getChildrenOfType(element.parent, PsiElement::class.java) + ?.filter { + ( + it.elementType == SqlTypes.EL_IDENTIFIER || + it is SqlElPrimaryExpr + ) && + it.textOffset <= element.textOffset + }?.toList() + ?.sortedBy { it.textOffset } ?: emptyList() + return nodeElm + } + + /** + * Follow the element at the cursor position and get the class of the nearest parent + */ + private fun getBindProperty( + elementBlock: List, + topElementClass: PsiParentClass, + ): BindVariableElement? { + // If the argument is List, get the element type + var parentClass = topElementClass + val accessElms = elementBlock.drop(1) + var isExistProperty: Boolean + + fun getBindVariableIfLastIndex( + index: Int, + type: PsiType, + originalElement: PsiElement, + ): BindVariableElement? { + isExistProperty = true + if (index >= accessElms.size - 1) { + return BindVariableElement(originalElement) + } + parentClass = PsiParentClass(type) + return null + } + + for (index in accessElms.indices) { + isExistProperty = false + val elm = accessElms[index] + + parentClass + .findField(elm.text) + ?.let { + val bindVal = getBindVariableIfLastIndex(index, it.type, it.originalElement) + if (bindVal != null) return bindVal + } + if (isExistProperty) continue + parentClass + .findMethod(elm.text) + ?.let { + if (it.returnType == null) return null + val bindVal = + getBindVariableIfLastIndex(index, it.returnType!!, it.originalElement) + if (bindVal != null) return bindVal + } + if (!isExistProperty) return null + } + return null + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/bundle/MessageBundle.kt b/src/main/kotlin/org/domaframework/doma/intellij/bundle/MessageBundle.kt new file mode 100644 index 00000000..b1c2f253 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/bundle/MessageBundle.kt @@ -0,0 +1,38 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.bundle + +import com.intellij.DynamicBundle +import org.jetbrains.annotations.NonNls +import org.jetbrains.annotations.PropertyKey + +@NonNls +private const val BUNDLE = "messages.DomaToolsBundle" + +object MessageBundle : DynamicBundle(BUNDLE) { + @JvmStatic + fun message( + @PropertyKey(resourceBundle = BUNDLE) key: String, + vararg params: Any, + ) = getMessage(key, *params) + + @Suppress("unused") + @JvmStatic + fun messagePointer( + @PropertyKey(resourceBundle = BUNDLE) key: String, + vararg params: Any, + ) = getLazyMessage(key, *params) +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/CommonPathParameter.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/CommonPathParameter.kt new file mode 100644 index 00000000..7f0810f3 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/CommonPathParameter.kt @@ -0,0 +1,29 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common + +open class CommonPathParameter { + companion object { + val SRC_MAIN_PATH: String + get() = "/src/main" + + val RESOURCES_PATH: String + get() = "resources" + + val RESOURCES_META_INF_PATH: String + get() = "META-INF" + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/FileTypeCheck.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/FileTypeCheck.kt new file mode 100644 index 00000000..be3022d3 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/FileTypeCheck.kt @@ -0,0 +1,76 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common + +import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiFile +import org.domaframework.doma.intellij.common.CommonPathParameter.Companion.SRC_MAIN_PATH + +/** + * Get extension by file type identifier + */ +fun getExtension(type: String): String = + when (type) { + "JAVA" -> "java" + "Kotlin" -> "kt" + "SQL" -> "sql" + else -> { + "" + } + } + +/** + * Does it match the Dao file type condition? + */ +fun isJavaOrKotlinFileType(daoFile: PsiFile): Boolean { + if (daoFile.virtualFile == null) return false + val fileType = FileTypeManager.getInstance().getFileTypeByFile(daoFile.virtualFile) + return when (fileType.name) { + "JAVA", "Kotlin" -> true + else -> false + } +} + +/* + * Determine whether the open file is an SQL template file extension + */ +fun isSupportFileType(file: VirtualFile): Boolean { + val extension = file.extension + return when (extension) { + "sql", "script" -> true + else -> false + } +} + +/** + * Dao file search for SQL files + */ +fun searchDaoFile( + contentRoot: VirtualFile?, + originFilePath: String, + relativeDaoFilePath: String, +): VirtualFile? { + val projectRootPath = contentRoot?.path ?: return null + if (projectRootPath.endsWith(SRC_MAIN_PATH)) { + return contentRoot.findFileByRelativePath(relativeDaoFilePath) + } + val subProject = + originFilePath.substring(projectRootPath.length, originFilePath.indexOf(SRC_MAIN_PATH)) + return contentRoot + .findFileByRelativePath(subProject) + ?.findFileByRelativePath(relativeDaoFilePath) +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/PluginLoggerUtil.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/PluginLoggerUtil.kt new file mode 100644 index 00000000..d9459c13 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/PluginLoggerUtil.kt @@ -0,0 +1,60 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.awt.event.InputEvent + +open class PluginLoggerUtil { + companion object { + private val logger: Logger = LoggerFactory.getLogger(this::class.java) + + fun countLoggingByAction( + className: String, + actionName: String, + inputEvent: InputEvent?, + start: Long, + ) { + logging(className, actionName, inputEvent?.javaClass?.simpleName, start) + } + + fun countLogging( + className: String, + actionName: String, + inputName: String, + start: Long, + ) { + logging(className, actionName, inputName, start) + } + + private fun logging( + className: String, + actionName: String, + inputName: String?, + start: Long, + ) { + val duration = (System.nanoTime() - start) / 1_000_000F + logger.info( + "\"{}\",\"{}\",\"{}\",{}", + className, + inputName ?: "null", + actionName, + duration, + ) + } + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/PluginUtil.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/PluginUtil.kt new file mode 100644 index 00000000..9464be03 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/PluginUtil.kt @@ -0,0 +1,33 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common + +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.extensions.PluginId + +const val PLUGIN_ID = "org.domaframework.doma.intellij" +const val PLUGIN_VERSION = "0.3.0" + +open class PluginUtil { + companion object { + fun getVersion(): String { + val pluginId = PluginId.getId(PLUGIN_ID) + return PluginManagerCore.getPlugin(pluginId)?.let { + return it.version + } ?: PLUGIN_VERSION + } + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/dao/DaoClass.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/dao/DaoClass.kt new file mode 100644 index 00000000..4451fa22 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/dao/DaoClass.kt @@ -0,0 +1,25 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common.dao + +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiFile +import com.intellij.psi.util.PsiTreeUtil + +fun getDaoClass(file: PsiFile): PsiClass? = + PsiTreeUtil + .findChildrenOfType(file, PsiClass::class.java) + .firstOrNull { it.hasAnnotation("org.seasar.doma.Dao") } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/dao/DaoMethodUtil.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/dao/DaoMethodUtil.kt new file mode 100644 index 00000000..04ce6c01 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/dao/DaoMethodUtil.kt @@ -0,0 +1,136 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common.dao + +import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiJavaFile +import com.intellij.psi.PsiMethod +import com.intellij.psi.util.PsiTreeUtil +import org.domaframework.doma.intellij.common.CommonPathParameter.Companion.RESOURCES_META_INF_PATH +import org.domaframework.doma.intellij.common.CommonPathParameter.Companion.RESOURCES_PATH +import org.domaframework.doma.intellij.common.getExtension +import org.domaframework.doma.intellij.common.isSupportFileType +import org.domaframework.doma.intellij.common.searchDaoFile +import org.domaframework.doma.intellij.extension.findFile +import org.domaframework.doma.intellij.extension.getContentRoot +import org.domaframework.doma.intellij.extension.getJavaClazz +import org.domaframework.doma.intellij.extension.getModule + +/** + * Get Dao method corresponding to SQL file + */ +fun findDaoMethod(originalFile: PsiFile): PsiMethod? { + val project = originalFile.project + val module = project.getModule(originalFile.virtualFile) ?: return null + val fileType = FileTypeManager.getInstance().getFileTypeByFile(originalFile.virtualFile) + + if (isSupportFileType(originalFile.virtualFile)) { + // TODO: Add Support Kotlin + val fileTypeName = "JAVA" + val daoFile = findDaoFile(project, originalFile.virtualFile) ?: return null + val relativePath = + formatDaoPathFromSqlFilePath( + originalFile.virtualFile, + project.getContentRoot(originalFile.virtualFile)?.path ?: "", + fileTypeName, + ) + val daoClassName: String = + relativePath + .substringBefore(".") + .replace("/", ".") + .replace("\\", ".") + .substringAfter(".${getExtension(fileTypeName)}") + .replace("..", ".") + .trim('.') + + val daoJavaFile = project.findFile(daoFile) + findDaoClass(module, daoClassName)?.let { daoClass -> + val methodName = originalFile.name.substringBeforeLast(".") + val daoMethod = + when (daoJavaFile) { + is PsiJavaFile -> findUseSqlDaoMethod(daoJavaFile, methodName) + else -> null + } + return daoMethod + } + } else if (fileType.defaultExtension == "sql") { + originalFile.let { + return PsiTreeUtil.getParentOfType(originalFile.context, PsiMethod::class.java) + } + } + return null +} + +/** + * Get jump destination Dao method file from SQL file + */ +fun findDaoFile( + project: Project, + sqlFile: VirtualFile, +): VirtualFile? { + project.getModule(sqlFile) ?: return null + val contentRoot = project.getContentRoot(sqlFile) ?: return null + // TODO: Add Support Kotlin + val relativeFilePath = + formatDaoPathFromSqlFilePath(sqlFile, contentRoot.path, "JAVA") + return searchDaoFile(contentRoot, sqlFile.path, relativeFilePath) +} + +private fun findDaoClass( + module: Module, + daoClassName: String, +): PsiClass? = module.getJavaClazz(true, daoClassName) + +/** + * Generate Dao deployment path from SQL file path + */ +fun formatDaoPathFromSqlFilePath( + relativeBaseSqlFile: VirtualFile, + projectRootPath: String, + extension: String, +): String { + var relativeFilePath = relativeBaseSqlFile.path.substring(projectRootPath.length) + if (!relativeFilePath.startsWith("/")) { + relativeFilePath = "/$relativeFilePath" + } + val extensionType = getExtension(extension.uppercase()) + return relativeFilePath + .replace("/$RESOURCES_PATH", "") + .replace(RESOURCES_META_INF_PATH, extension.lowercase()) + .replace("/${relativeBaseSqlFile.name}", "") + .plus(".$extensionType") +} + +/** + * Generate SqlFile path from Dao file path + */ +fun formatSqlPathFromDaoPath( + contentRootPath: String, + daoFile: VirtualFile, +): String { + val fileType = daoFile.fileType.name + val extension = daoFile.fileType.defaultExtension + val daoFilePath = daoFile.path + return daoFilePath + .replace(contentRootPath, RESOURCES_META_INF_PATH) + .replace("/${fileType.lowercase()}/", "/") + .replace(".$extension", "") +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/dao/JumpActionFunctions.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/dao/JumpActionFunctions.kt new file mode 100644 index 00000000..9537449d --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/dao/JumpActionFunctions.kt @@ -0,0 +1,89 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common.dao + +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiJavaFile +import com.intellij.psi.PsiMethod +import com.intellij.util.PsiNavigateUtil +import org.domaframework.doma.intellij.common.psi.PsiDaoMethod +import org.domaframework.doma.intellij.extension.findFile +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.utils.rethrow + +fun jumpSqlFromDao( + project: Project, + sqlFile: VirtualFile, +) { + FileEditorManager.getInstance(project).openFile(sqlFile, true) +} + +fun jumpToDaoMethod( + project: Project, + sqlFileName: String, + daoFile: VirtualFile, +) { + when (val daoPsiFile = project.findFile(daoFile)) { + is PsiJavaFile -> getJavaFunctionOffset(daoPsiFile, sqlFileName) + is KtFile -> getKotlinFunctions(daoPsiFile, sqlFileName) + } +} + +private fun getKotlinFunctions( + file: KtFile, + targetMethodName: String, +) { + val method = + file.declarations + .filterIsInstance() + .find { f -> f.name == targetMethodName } + if (method != null) { + PsiNavigateUtil.navigate(method) + } +} + +private fun getJavaFunctionOffset( + file: PsiJavaFile, + targetMethodName: String, +) { + try { + PsiNavigateUtil.navigate(findUseSqlDaoMethod(file, targetMethodName)!!) + } catch (e: Exception) { + rethrow(e) + } +} + +fun findUseSqlDaoMethod( + file: PsiJavaFile, + targetMethodName: String, +): PsiMethod? { + for (clazz in file.classes) { + val methods = clazz.findMethodsByName(targetMethodName, true) + if (methods.isNotEmpty()) { + val targetMethod = + methods.firstOrNull { method -> + val psiDaoMethod = PsiDaoMethod(file.project, method) + psiDaoMethod.isUseSqlFileMethod() + } + return targetMethod + break + } + } + return null +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PropertyModifyUtil.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PropertyModifyUtil.kt new file mode 100644 index 00000000..0940a956 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PropertyModifyUtil.kt @@ -0,0 +1,45 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common.psi + +import com.intellij.psi.PsiField +import com.intellij.psi.PsiModifier +import com.intellij.psi.PsiType +import com.intellij.psi.util.PsiUtil + +open class PropertyModifyUtil { + companion object { + /** + * If it is a Java package, exclude fields other than public. + */ + fun filterPrivateField( + field: PsiField, + type: PsiType, + ): Boolean { + if (isJavaPackage(type)) { + return field.hasModifierProperty(PsiModifier.PUBLIC) + } + return true + } + + private fun isJavaPackage(type: PsiType): Boolean { + val clazzType = PsiUtil.resolveClassInType(type) + return clazzType?.qualifiedName?.startsWith("java.") == true || + clazzType?.qualifiedName?.startsWith("javax.") == true || + clazzType?.qualifiedName?.startsWith("jakarta.") == true + } + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiDaoMethod.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiDaoMethod.kt new file mode 100644 index 00000000..42565aa8 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiDaoMethod.kt @@ -0,0 +1,181 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common.psi + +import com.intellij.lang.Language +import com.intellij.lang.injection.InjectedLanguageManager +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.PsiLiteralExpression +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiNameValuePair +import com.intellij.util.IncorrectOperationException +import org.domaframework.doma.intellij.common.CommonPathParameter.Companion.RESOURCES_PATH +import org.domaframework.doma.intellij.common.dao.formatSqlPathFromDaoPath +import org.domaframework.doma.intellij.extension.findFile +import org.domaframework.doma.intellij.extension.getContentRoot +import org.domaframework.doma.intellij.extension.getModule +import org.domaframework.doma.intellij.extension.getResourcesSQLFile +import org.domaframework.doma.intellij.extension.psi.DomaAnnotationType +import org.domaframework.doma.intellij.setting.SqlLanguage +import java.io.File +import java.io.IOException + +/** + * Class that handles Dao method information + */ +class PsiDaoMethod( + val psiProject: Project, + val psiMethod: PsiMethod, +) { + private val isTest = false + var sqlFile: VirtualFile? = null + private var sqlFilePath: String = "" + + private val daoFile: VirtualFile = psiMethod.containingFile.virtualFile + var daoType: DomaAnnotationType = DomaAnnotationType.Unknown + private var sqlFileOption: Boolean = false + + init { + setDaoAnnotationType() + setSqlFileOption() + setSqlFilePath() + setSqlFile() + } + + private fun setSqlFileOption() { + val useSqlFileOptionAnnotation = daoType.getPsiAnnotation(psiMethod) ?: return + val isSqlFile = daoType.getSqlFileVal(useSqlFileOptionAnnotation) + sqlFileOption = isSqlFile == true + } + + private fun setDaoAnnotationType() { + DomaAnnotationType.entries.forEach { type -> + if (type != DomaAnnotationType.Sql && + type != DomaAnnotationType.Unknown && + type.getPsiAnnotation(psiMethod) != null + ) { + daoType = type + return + } + } + daoType = DomaAnnotationType.Unknown + } + + fun isUseSqlFileMethod(): Boolean = + when { + useSqlAnnotation() -> false + daoType.isRequireSqlTemplate() -> true + else -> sqlFileOption + } + + private fun getSqlAnnotation(): PsiAnnotation? = DomaAnnotationType.Sql.getPsiAnnotation(psiMethod) + + fun useSqlAnnotation(): Boolean = getSqlAnnotation() != null + + private fun setSqlFilePath() { + val methodName = psiMethod.name + + val sqlExtension = daoType.extension + val contentRoot = this.psiProject.getContentRoot(daoFile)?.path + + sqlFilePath = contentRoot?.let { + formatSqlPathFromDaoPath(it, daoFile) + .replace("main/", "") + .plus("/$methodName.$sqlExtension") + } ?: "" + } + + private fun setSqlFile() { + if (isUseSqlFileMethod()) { + val module = psiProject.getModule(daoFile) + sqlFile = + module?.getResourcesSQLFile( + sqlFilePath, + isTest, + ) + return + } + // the injection part as a custom language file + getSqlAnnotation()?.let { annotation -> + InjectedLanguageManager.getInstance(psiProject) + annotation.parameterList.children + .firstOrNull { it is PsiNameValuePair } + ?.let { sql -> + val valuePair = sql as PsiNameValuePair + val valueExpression = + valuePair.value as? PsiLiteralExpression ?: return + val valueText = valueExpression.value as? String ?: return + val psiFileFactory = PsiFileFactory.getInstance(psiProject) + sqlFile = + psiFileFactory + .createFileFromText( + "tempFile", + Language.findLanguageByID(SqlLanguage.INSTANCE.id)!!, + valueText, + ).virtualFile + } + } + } + + fun generateSqlFile() { + ApplicationManager.getApplication().runReadAction { + val rootDir = psiProject.getContentRoot(daoFile) ?: return@runReadAction + val sqlFile = File(sqlFilePath) + val sqlFileName = sqlFile.name + val parentDir = "${RESOURCES_PATH}/${sqlFile.parent.replace("\\", "/")}" + val parenDirPathSpirit = parentDir.split("/").toTypedArray() + + WriteCommandAction.runWriteCommandAction(psiProject) { + try { + VfsUtil.createDirectoryIfMissing(rootDir, parentDir) + } catch (e: IOException) { + throw IncorrectOperationException(e) + } + + val sqlOutputDirPath = + PsiManager + .getInstance(psiProject) + .findDirectory(VfsUtil.findRelativeFile(rootDir, *parenDirPathSpirit)!!) + val sqlVirtualFile = sqlOutputDirPath!!.createFile(sqlFileName).virtualFile + FileEditorManager + .getInstance(psiProject) + .openFile(sqlVirtualFile, true) + writeEmptyElementSqlFile(sqlVirtualFile) + } + } + } + + /** + * Add one line to display Gutter in the newly created SQL file + */ + private fun writeEmptyElementSqlFile(sqlVirtualFile: VirtualFile) { + val psiFile = psiProject.findFile(sqlVirtualFile) ?: return + val document = + PsiDocumentManager.getInstance(psiProject).getDocument(psiFile) ?: return + WriteCommandAction.runWriteCommandAction(psiProject) { + document.insertString(0, "-- Generated By Doma Tools") + } + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiParentClass.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiParentClass.kt new file mode 100644 index 00000000..d4820b62 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiParentClass.kt @@ -0,0 +1,102 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common.psi + +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiField +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiModifier +import com.intellij.psi.PsiType +import com.intellij.psi.util.PsiTypesUtil + +/** + * When parsing a field access element with SQL, + * manage the reference class information of the previous element + */ +class PsiParentClass( + val type: PsiType, +) { + var clazz: PsiClass? = psiClass() + + private fun psiClass() = PsiTypesUtil.getPsiClass(type) + + private val fields: Array? = clazz?.allFields + private val methods: Array? = clazz?.allMethods + + private fun getMethods(): List? = + methods?.filter { m -> + m.hasModifierProperty(PsiModifier.PUBLIC) + } + + fun findField(fieldName: String): PsiField? = + fields?.firstOrNull { f -> + f.name == fieldName && + PropertyModifyUtil.filterPrivateField(f, type) + } + + fun searchField(fieldName: String): List? = + fields?.filter { f -> + f.name.startsWith(fieldName) && + PropertyModifyUtil.filterPrivateField(f, type) + } + + fun findMethod(methodName: String): PsiMethod? = + getMethods() + ?.filter { m -> + m.hasModifierProperty(PsiModifier.PUBLIC) + }?.firstOrNull { m -> + m.name.substringBefore("(") == methodName.substringBefore("(") + } + + fun searchMethod(methodName: String): List? = + getMethods()?.filter { m -> + m.name.substringBefore("(").startsWith(methodName.substringBefore("(")) && + m.hasModifierProperty(PsiModifier.PUBLIC) + } + + fun findStaticField(fieldName: String): PsiField? = + fields + ?.filter { f -> + f.hasModifierProperty(PsiModifier.STATIC) && + PropertyModifyUtil.filterPrivateField(f, type) + }?.firstOrNull { f -> + f.name == fieldName + } + + fun searchStaticField(fieldName: String): List? = + fields + ?.filter { f -> + f.hasModifierProperty(PsiModifier.STATIC) && + f.name.startsWith(fieldName) && + PropertyModifyUtil.filterPrivateField(f, type) + } + + fun findStaticMethod(methodName: String): PsiMethod? = + methods + ?.filter { m -> + m.hasModifierProperty(PsiModifier.STATIC) && + m.hasModifierProperty(PsiModifier.PUBLIC) + }?.firstOrNull { m -> + m.name == methodName + } + + fun searchStaticMethod(methodName: String): List? = + methods?.filter { m -> + m.hasModifierProperty(PsiModifier.STATIC) && + m.hasModifierProperty(PsiModifier.PUBLIC) && + m.name.substringBefore("(").startsWith(methodName.substringBefore("(")) + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiPatternUtil.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiPatternUtil.kt new file mode 100644 index 00000000..117d69ae --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiPatternUtil.kt @@ -0,0 +1,106 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common.psi + +import com.intellij.patterns.PatternCondition +import com.intellij.patterns.PlatformPatterns +import com.intellij.patterns.PsiElementPattern +import com.intellij.psi.PsiElement +import com.intellij.psi.TokenType +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.elementType +import com.intellij.psi.util.prevLeaf +import com.intellij.psi.util.prevLeafs +import com.intellij.util.ProcessingContext +import org.domaframework.doma.intellij.psi.SqlTypes + +object PsiPatternUtil { + /** + * Creates a pattern that matches elements under a PsiComment or a specific parent class, + * regardless of depth. + * + * @param parentClass The specific class to match as a parent. + * @return A PsiElementPattern that matches the desired elements. + */ + fun createPattern(parentClass: Class): PsiElementPattern.Capture = + PlatformPatterns.psiElement().with( + object : PatternCondition("PsiParentCondition") { + override fun accepts( + element: PsiElement, + context: ProcessingContext?, + ): Boolean = PsiTreeUtil.getParentOfType(element, parentClass, true) != null + }, + ) + + fun createDirectivePattern(): PsiElementPattern.Capture { + return PlatformPatterns.psiElement().with( + object : PatternCondition("PsiParentCondition") { + override fun accepts( + element: PsiElement, + context: ProcessingContext?, + ): Boolean { + val bindText = element.prevLeaf()?.text ?: "" + val directiveSymbol = listOf("%", "@", "^", "#") + return directiveSymbol.any { + bindText.startsWith(it) || + (element.elementType == SqlTypes.EL_IDENTIFIER && element.prevLeaf()?.text == it) || + ( + element.elementType == TokenType.BAD_CHARACTER && + element.parent.prevLeafs + .firstOrNull { p -> p.text == it || p.elementType == SqlTypes.BLOCK_COMMENT_START } + ?.text == it + ) + } + } + }, + ) + } + + fun isMatchFileExtension(extension: String): PsiElementPattern.Capture = + PlatformPatterns.psiElement().with( + object : PatternCondition("PsiParentCondition") { + override fun accepts( + element: PsiElement, + context: ProcessingContext?, + ): Boolean = element.containingFile.originalFile.virtualFile.extension == extension + }, + ) + + /** + * Get the string to search from the cursor position to the start of a block comment or a blank space + * @return search Keyword + */ + fun getBindSearchWord( + originalFile: PsiElement, + element: PsiElement, + symbol: String, + ): String { + val text = originalFile.containingFile.text + val offset = element.textOffset + val builder = StringBuilder() + for (i in offset - 1 downTo 0) { + val char = text[i] + if (char.isWhitespace()) break + builder.insert(0, char) + } + val prefix = + builder + .toString() + .replace("/*", "") + .substringAfter(symbol) + return prefix + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiStaticElement.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiStaticElement.kt new file mode 100644 index 00000000..4514b8f4 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiStaticElement.kt @@ -0,0 +1,42 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common.psi + +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiFile +import org.domaframework.doma.intellij.extension.getJavaClazz +import org.domaframework.doma.intellij.psi.SqlElExpr +import org.jetbrains.kotlin.idea.base.util.module + +/** + * Directive information for static property references + */ +class PsiStaticElement( + elExprList: List? = null, + originalFile: PsiFile, +) { + private var fqdn = elExprList?.joinToString(".") { e -> e.text } ?: "" + private val module = originalFile.module + + constructor(elExprNames: String, file: PsiFile) : this(null, file) { + fqdn = + elExprNames + .substringAfter("@") + .substringBefore("@") + } + + fun getRefClazz(): PsiClass? = module?.getJavaClazz(true, fqdn) +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiTypeChecker.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiTypeChecker.kt new file mode 100644 index 00000000..6610d908 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/psi/PsiTypeChecker.kt @@ -0,0 +1,88 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common.psi + +import com.intellij.psi.PsiArrayType +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiPrimitiveType +import com.intellij.psi.PsiType +import com.intellij.psi.util.PsiUtil + +object PsiTypeChecker { + private val TARGET_CLASSES: MutableSet = HashSet() + private val WRAPPER_CLASSES: MutableSet = HashSet() + + init { + TARGET_CLASSES.add("java.lang.String") + TARGET_CLASSES.add("java.lang.Object") + TARGET_CLASSES.add("java.math.BigDecimal") + TARGET_CLASSES.add("java.math.BigInteger") + TARGET_CLASSES.add("java.time.LocalDate") + TARGET_CLASSES.add("java.time.LocalTime") + TARGET_CLASSES.add("java.time.LocalDateTime") + TARGET_CLASSES.add("java.sql.Date") + TARGET_CLASSES.add("java.sql.Time") + TARGET_CLASSES.add("java.sql.Timestamp") + TARGET_CLASSES.add("java.sql.Array") + TARGET_CLASSES.add("java.sql.Blob") + TARGET_CLASSES.add("java.sql.Clob") + TARGET_CLASSES.add("java.sql.SQLXML") + TARGET_CLASSES.add("java.util.Date") + + WRAPPER_CLASSES.add("java.lang.Byte") + WRAPPER_CLASSES.add("java.lang.Short") + WRAPPER_CLASSES.add("java.lang.Integer") + WRAPPER_CLASSES.add("java.lang.Long") + WRAPPER_CLASSES.add("java.lang.Float") + WRAPPER_CLASSES.add("java.lang.Double") + WRAPPER_CLASSES.add("java.lang.Boolean") + } + + /** + * Determines whether the specified PsiType satisfies the conditions. + * @param psiType Check target PsiType + * @return true if the condition is met + */ + fun isTargetType(psiType: PsiType?): Boolean { + if (psiType == null) return false + if (psiType is PsiPrimitiveType && psiType.canonicalText == "char") { + return false + } + if (psiType is PsiClassType) { + val psiClass = PsiUtil.resolveClassInType(psiType) + if (psiClass != null) { + val qualifiedName = psiClass.qualifiedName + if (qualifiedName != null) { + if (WRAPPER_CLASSES.contains(qualifiedName) || + TARGET_CLASSES.contains(qualifiedName) + ) { + return true + } + } + if (psiClass.isEnum) { + return true + } + } + } + if (psiType is PsiArrayType) { + val componentType = psiType.componentType.canonicalText + if ("java.lang.Byte" == componentType) { + return true + } + } + return true + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/sql/CleanElementText.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/sql/CleanElementText.kt new file mode 100644 index 00000000..61a2de9e --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/sql/CleanElementText.kt @@ -0,0 +1,32 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common.sql + +/** + * Exclude extra strings and block symbols added by IntelliJ operations a + * nd format them into necessary elements + */ +fun cleanString(str: String): String { + val intelliKIdeaRuleZzz = "IntellijIdeaRulezzz" + return str + .substringAfter("/*") + .substringBefore("*/") + .replace(intelliKIdeaRuleZzz, "") + // TODO: Temporary support when using operators. + // Remove the "== a" element because it is attached to the end. + // Make it possible to obtain the equilateral elements of the left side individually. + .substringBefore(" ") +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/BindDirectiveUtil.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/BindDirectiveUtil.kt new file mode 100644 index 00000000..f0c9eec0 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/BindDirectiveUtil.kt @@ -0,0 +1,61 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common.sql.directive + +import com.intellij.psi.PsiElement +import com.intellij.psi.util.elementType +import com.intellij.psi.util.prevLeafs +import org.domaframework.doma.intellij.extension.psi.isNotWhiteSpace +import org.domaframework.doma.intellij.psi.SqlElStaticFieldAccessExpr +import org.domaframework.doma.intellij.psi.SqlTypes + +enum class DirectiveType { + PERCENT, + STATIC, + BUILT_IN, + NOT_DIRECTIVE, +} + +object BindDirectiveUtil { + /** + * Determine which directive the block to validate belongs to + */ + fun getDirectiveType(element: PsiElement): DirectiveType { + // During input, "%" is recognized as an error token, + // so search by simple string comparison instead of element type. + element.prevLeafs + .firstOrNull { p -> + ( + p.text == "%" || + p.prevSibling?.text == "%" || + p is SqlElStaticFieldAccessExpr || + p.elementType == SqlTypes.EL_AT_SIGN + ) || + p.elementType == SqlTypes.BLOCK_COMMENT_START || + !p.isNotWhiteSpace() + }?.let { + return when { + !it.isNotWhiteSpace() -> DirectiveType.NOT_DIRECTIVE + it.text == "%" || it.prevSibling?.text == "%" -> DirectiveType.PERCENT + it is SqlElStaticFieldAccessExpr -> DirectiveType.STATIC + it.elementType == SqlTypes.EL_AT_SIGN -> DirectiveType.BUILT_IN + else -> DirectiveType.NOT_DIRECTIVE + } + } + + return DirectiveType.NOT_DIRECTIVE + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/DirectiveCompletion.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/DirectiveCompletion.kt new file mode 100644 index 00000000..8f5e687c --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/DirectiveCompletion.kt @@ -0,0 +1,65 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common.sql.directive + +import com.intellij.codeInsight.completion.CompletionResultSet +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile + +class DirectiveCompletion( + private val originalFile: PsiFile, + private val bindText: String, + private val element: PsiElement, + private val project: Project, + private val result: CompletionResultSet, +) { + fun directiveHandle(symbol: String): Boolean { + return when (symbol) { + "%" -> + PercentDirectiveHandler( + originalFile = originalFile, + element = element, + result = result, + ).directiveHandle() + + "#" -> + EmbeddedDirectiveHandler( + originalFile = originalFile, + element = element, + result = result, + ).directiveHandle() + + "^" -> + LiteralDirectiveHandler( + originalFile = originalFile, + element = element, + result = result, + ).directiveHandle() + + "@" -> + StaticDirectiveHandler( + originalFile = originalFile, + element = element, + result = result, + bindText = bindText, + project = project, + ).directiveHandle() + + else -> return false + } + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/DirectiveHandler.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/DirectiveHandler.kt new file mode 100644 index 00000000..11db41d8 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/DirectiveHandler.kt @@ -0,0 +1,46 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common.sql.directive + +import com.intellij.psi.PsiElement +import org.domaframework.doma.intellij.common.psi.PsiPatternUtil +import org.jetbrains.kotlin.psi.psiUtil.prevLeaf + +/** + * Determine directive elements and perform code completion + */ +open class DirectiveHandler( + private val originalFile: PsiElement, +) { + open fun directiveHandle(): Boolean = false + + fun isDirective( + it: PsiElement, + symbol: String, + ): Boolean { + val prev = it.prevLeaf() + return ( + prev != null && + prev.text == symbol || + it.text.startsWith(symbol) + ) + } + + protected fun getBindSearchWord( + element: PsiElement, + symbol: String, + ): String = PsiPatternUtil.getBindSearchWord(originalFile, element, symbol) +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/EmbeddedDirectiveHandler.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/EmbeddedDirectiveHandler.kt new file mode 100644 index 00000000..1459d845 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/EmbeddedDirectiveHandler.kt @@ -0,0 +1,63 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common.sql.directive + +import com.intellij.codeInsight.completion.CompletionResultSet +import com.intellij.codeInsight.lookup.VariableLookupItem +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiMethod +import org.domaframework.doma.intellij.common.dao.findDaoMethod + +class EmbeddedDirectiveHandler( + private val originalFile: PsiFile, + private val element: PsiElement, + private val result: CompletionResultSet, +) : DirectiveHandler(originalFile) { + private val symbol = "#" + + override fun directiveHandle(): Boolean = + directiveHandler( + element, + originalFile, + result, + ) { daoMethod, bind -> + daoMethod + ?.parameterList + ?.parameters + ?.filter { + it.name.startsWith(bind) + }?.map { param -> VariableLookupItem(param) } + ?.toList() + ?: emptyList() + } + + private fun directiveHandler( + it: PsiElement, + originalFile: PsiFile, + result: CompletionResultSet, + processor: (PsiMethod?, String) -> List, + ): Boolean { + if (isDirective(it, symbol)) { + val daoMethod = findDaoMethod(originalFile) + val prefix = getBindSearchWord(element, symbol) + val candidates = processor(daoMethod, prefix) + result.addAllElements(candidates) + return true + } + return false + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/LiteralDirectiveHandler.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/LiteralDirectiveHandler.kt new file mode 100644 index 00000000..cfa63524 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/LiteralDirectiveHandler.kt @@ -0,0 +1,65 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common.sql.directive + +import com.intellij.codeInsight.completion.CompletionResultSet +import com.intellij.codeInsight.lookup.VariableLookupItem +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiMethod +import org.domaframework.doma.intellij.common.dao.findDaoMethod +import org.domaframework.doma.intellij.common.psi.PsiTypeChecker + +class LiteralDirectiveHandler( + private val originalFile: PsiFile, + private val element: PsiElement, + private val result: CompletionResultSet, +) : DirectiveHandler(originalFile) { + private val symbol = "^" + + override fun directiveHandle(): Boolean = + directiveHandler( + element, + originalFile, + result, + ) { daoMethod, bind -> + daoMethod + ?.parameterList + ?.parameters + ?.filter { + it.name.startsWith(bind) && + PsiTypeChecker.isTargetType(it.type) + }?.map { param -> VariableLookupItem(param) } + ?.toList() + ?: emptyList() + } + + private fun directiveHandler( + it: PsiElement, + originalFile: PsiFile, + result: CompletionResultSet, + processor: (PsiMethod?, String) -> List, + ): Boolean { + if (isDirective(it, symbol)) { + val daoMethod = findDaoMethod(originalFile) + val prefix = getBindSearchWord(element, symbol) + val candidates = processor(daoMethod, prefix) + result.addAllElements(candidates) + return true + } + return false + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/PercentDirectiveHandler.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/PercentDirectiveHandler.kt new file mode 100644 index 00000000..50213b71 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/PercentDirectiveHandler.kt @@ -0,0 +1,68 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common.sql.directive + +import com.intellij.codeInsight.completion.CompletionResultSet +import com.intellij.codeInsight.completion.PlainPrefixMatcher +import com.intellij.codeInsight.lookup.AutoCompletionPolicy +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.psi.PsiElement + +class PercentDirectiveHandler( + originalFile: PsiElement, + private val element: PsiElement, + private val result: CompletionResultSet, +) : DirectiveHandler(originalFile) { + override fun directiveHandle(): Boolean = + percentDirectiveHandler( + element, + result, + ) { bind -> + listOf( + "if", + "elseif", + "else", + "end", + "expand", + "populate", + "for", + "!", + ).filter { + it.startsWith(bind) + }.map { + LookupElementBuilder + .create(it) + .withAutoCompletionPolicy(AutoCompletionPolicy.ALWAYS_AUTOCOMPLETE) + } + } + + private fun percentDirectiveHandler( + element: PsiElement, + result: CompletionResultSet, + processor: (String) -> List, + ): Boolean { + if (BindDirectiveUtil.getDirectiveType(element) == DirectiveType.PERCENT) { + val prefix = getBindSearchWord(element, "%") + result.withPrefixMatcher(PlainPrefixMatcher(prefix)).apply { + val candidates = processor(prefix) + result.addAllElements(candidates) + } + return true + } + return false + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/StaticDirectiveHandler.kt b/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/StaticDirectiveHandler.kt new file mode 100644 index 00000000..82515d10 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/common/sql/directive/StaticDirectiveHandler.kt @@ -0,0 +1,233 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.common.sql.directive + +import com.intellij.codeInsight.completion.CompletionResultSet +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.codeInsight.lookup.VariableLookupItem +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiType +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.elementType +import org.domaframework.doma.intellij.common.psi.PsiParentClass +import org.domaframework.doma.intellij.common.psi.PsiStaticElement +import org.domaframework.doma.intellij.extension.psi.psiClassType +import org.domaframework.doma.intellij.psi.SqlElClass +import org.domaframework.doma.intellij.psi.SqlElStaticFieldAccessExpr +import org.domaframework.doma.intellij.psi.SqlTypes + +class StaticDirectiveHandler( + private val originalFile: PsiElement, + private val element: PsiElement, + private val result: CompletionResultSet, + private val bindText: String, + private val project: Project, +) : DirectiveHandler(originalFile) { + /** + * Function information displayed with code completion for built-in functions + */ + data class DomaFunction( + val name: String, + val returnType: PsiType, + val parameters: List, + ) + + /** + * Show parameters in code completion for fields and methods + */ + data class CompletionSuggest( + val field: List, + val methods: List, + ) + + override fun directiveHandle(): Boolean { + var handleResult = false + if (element.prevSibling is SqlElStaticFieldAccessExpr) { + handleResult = + staticDirectiveHandler(element, result) { fqdn, bind -> + val psiStaticElement = PsiStaticElement(fqdn, originalFile.containingFile) + val javaClass = + psiStaticElement.getRefClazz() ?: return@staticDirectiveHandler null + val parentClazz = PsiParentClass(javaClass.psiClassType) + parentClazz.let { clazz -> + val fields = + clazz.searchStaticField(bind)?.map { f -> VariableLookupItem(f) } + val methods = + clazz.searchStaticMethod(bind)?.map { m -> + LookupElementBuilder + .create("${m.name}()") + .withPresentableText(m.name) + .withTailText(m.parameterList.text, true) + .withTypeText(m.returnType?.presentableText ?: "") + } + CompletionSuggest(fields ?: emptyList(), methods ?: emptyList()) + } + } + } else if (element.prevSibling?.elementType == SqlTypes.EL_AT_SIGN) { + // Built-in function completion + handleResult = + builtInDirectiveHandler(element, result) { bind -> + listOf( + DomaFunction( + "escape", + getJavaLangString(), + listOf( + getPsiTypeByClassName("java.lang.CharSequence"), + getPsiTypeByClassName("java.lang.Char"), + ), + ), + DomaFunction( + "prefix", + getJavaLangString(), + listOf( + getPsiTypeByClassName("java.lang.CharSequence"), + getPsiTypeByClassName("java.lang.Char"), + ), + ), + DomaFunction( + "infix", + getJavaLangString(), + listOf( + getPsiTypeByClassName("java.lang.CharSequence"), + getPsiTypeByClassName("java.lang.Char"), + ), + ), + DomaFunction( + "suffix", + getJavaLangString(), + listOf( + getPsiTypeByClassName("java.lang.CharSequence"), + getPsiTypeByClassName("java.lang.Char"), + ), + ), + DomaFunction( + "roundDownTimePart", + getPsiTypeByClassName("java.util.Date"), + listOf(getPsiTypeByClassName("java.util.Date")), + ), + DomaFunction( + "roundDownTimePart", + getPsiTypeByClassName("java.sql.Date"), + listOf(getPsiTypeByClassName("java.util.Date")), + ), + DomaFunction( + "roundDownTimePart", + getPsiTypeByClassName("java.sql.Timestamp"), + listOf(getPsiTypeByClassName("java.sql.Timestamp")), + ), + DomaFunction( + "roundDownTimePart", + getPsiTypeByClassName("java.time.LocalDateTime"), + listOf(getPsiTypeByClassName("java.time.LocalDateTime")), + ), + DomaFunction( + "roundUpTimePart", + getPsiTypeByClassName("java.util.Date"), + listOf(getPsiTypeByClassName("java.sql.Date")), + ), + DomaFunction( + "roundUpTimePart", + getPsiTypeByClassName("java.sql.Timestamp"), + listOf(getPsiTypeByClassName("java.sql.Timestamp")), + ), + DomaFunction( + "roundUpTimePart", + getPsiTypeByClassName("java.time.LocalDate"), + listOf(getPsiTypeByClassName("java.time.LocalDate")), + ), + DomaFunction( + "isEmpty", + getPsiTypeByClassName("boolean"), + listOf(getPsiTypeByClassName("java.lang.CharSequence")), + ), + DomaFunction( + "isNotEmpty", + getPsiTypeByClassName("boolean"), + listOf(getPsiTypeByClassName("java.lang.CharSequence")), + ), + DomaFunction( + "isBlank", + getPsiTypeByClassName("boolean"), + listOf(getPsiTypeByClassName("java.lang.CharSequence")), + ), + DomaFunction( + "isNotBlank", + getPsiTypeByClassName("boolean"), + listOf(getPsiTypeByClassName("java.lang.CharSequence")), + ), + ).filter { + it.name.startsWith(bind.substringAfter("@")) + }.map { + LookupElementBuilder + .create("${it.name}()") + .withPresentableText(it.name) + .withTailText( + "(${ + it.parameters.joinToString(",") { param -> + param.toString().replace("PsiType:", "") + } + })", + true, + ).withTypeText(it.returnType.presentableText) + } + } + } + return handleResult + } + + private fun staticDirectiveHandler( + element: PsiElement, + result: CompletionResultSet, + processor: (String, String) -> CompletionSuggest?, + ): Boolean { + val clazzRef = + PsiTreeUtil + .getChildOfType(element.prevSibling, SqlElClass::class.java) // getStaticFieldAccessClazzRef(element) ?: return false + val fqdn = + PsiTreeUtil.getChildrenOfTypeAsList(clazzRef, PsiElement::class.java).joinToString("") { it.text } + val candidates = processor(fqdn, bindText) ?: return false + result.addAllElements(candidates.field) + candidates.methods.map { m -> result.addElement(m) } + return true + } + + private fun builtInDirectiveHandler( + element: PsiElement, + result: CompletionResultSet, + processor: (String) -> List?, + ): Boolean { + if (BindDirectiveUtil.getDirectiveType(element) == DirectiveType.BUILT_IN) { + val prefix = getBindSearchWord(element, "@") + val candidates = processor(prefix) + candidates?.let { it1 -> result.addAllElements(it1) } + return true + } + return false + } + + private fun getJavaLangString(): PsiType = + PsiType.getJavaLangString( + PsiManager.getInstance(project), + GlobalSearchScope.allScope(project), + ) + + private fun getPsiTypeByClassName(className: String): PsiType = + PsiType.getTypeByName(className, project, GlobalSearchScope.allScope(project)) +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/contributor/sql/SqlCompletionContributor.kt b/src/main/kotlin/org/domaframework/doma/intellij/contributor/sql/SqlCompletionContributor.kt new file mode 100644 index 00000000..c94590f5 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/contributor/sql/SqlCompletionContributor.kt @@ -0,0 +1,70 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.contributor.sql + +import com.intellij.codeInsight.completion.CompletionContributor +import com.intellij.codeInsight.completion.CompletionType +import com.intellij.patterns.PlatformPatterns +import com.intellij.patterns.StandardPatterns +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiElement +import org.domaframework.doma.intellij.common.psi.PsiPatternUtil +import org.domaframework.doma.intellij.contributor.sql.provider.SqlParameterCompletionProvider +import org.jetbrains.kotlin.idea.completion.or + +/** + * Code completion of SQL bind variables with Dao method arguments + * Valid only for SQL files or @Sql in Dao + */ +open class SqlCompletionContributor : CompletionContributor() { + init { + extend( + CompletionType.BASIC, + PsiPatternUtil + .createPattern(PsiComment::class.java) + .andOr( + PsiPatternUtil + .createPattern(PsiComment::class.java) + .inFile( + PlatformPatterns + .psiFile() + .withName(StandardPatterns.string().endsWith(".sql")), + ), + PsiPatternUtil + .createPattern(PsiComment::class.java) + .and(PsiPatternUtil.isMatchFileExtension("java")), + ) + // Support for directive elements + .or( + PlatformPatterns + .psiElement(PsiElement::class.java) + .andOr( + PsiPatternUtil + .createDirectivePattern() + .inFile( + PlatformPatterns + .psiFile() + .withName(StandardPatterns.string().endsWith(".sql")), + ), + PsiPatternUtil + .createDirectivePattern() + .and(PsiPatternUtil.isMatchFileExtension("java")), + ), + ), + SqlParameterCompletionProvider(), + ) + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/contributor/sql/provider/SqlParameterCompletionProvider.kt b/src/main/kotlin/org/domaframework/doma/intellij/contributor/sql/provider/SqlParameterCompletionProvider.kt new file mode 100644 index 00000000..72e43e4e --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/contributor/sql/provider/SqlParameterCompletionProvider.kt @@ -0,0 +1,347 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.contributor.sql.provider + +import com.intellij.codeInsight.completion.CompletionParameters +import com.intellij.codeInsight.completion.CompletionProvider +import com.intellij.codeInsight.completion.CompletionResultSet +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.codeInsight.lookup.VariableLookupItem +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiErrorElement +import com.intellij.psi.PsiField +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiType +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.elementType +import com.intellij.psi.util.prevLeafs +import com.intellij.util.ProcessingContext +import org.domaframework.doma.intellij.common.PluginLoggerUtil +import org.domaframework.doma.intellij.common.dao.findDaoMethod +import org.domaframework.doma.intellij.common.psi.PsiParentClass +import org.domaframework.doma.intellij.common.psi.PsiPatternUtil +import org.domaframework.doma.intellij.common.psi.PsiTypeChecker +import org.domaframework.doma.intellij.common.sql.cleanString +import org.domaframework.doma.intellij.common.sql.directive.DirectiveCompletion +import org.domaframework.doma.intellij.extension.getJavaClazz +import org.domaframework.doma.intellij.extension.psi.findNodeParent +import org.domaframework.doma.intellij.extension.psi.findSelfBlocks +import org.domaframework.doma.intellij.extension.psi.findStaticField +import org.domaframework.doma.intellij.extension.psi.findStaticMethod +import org.domaframework.doma.intellij.extension.psi.getDomaAnnotationType +import org.domaframework.doma.intellij.extension.psi.getIterableClazz +import org.domaframework.doma.intellij.extension.psi.getMethodReturnType +import org.domaframework.doma.intellij.extension.psi.isNotWhiteSpace +import org.domaframework.doma.intellij.extension.psi.searchParameter +import org.domaframework.doma.intellij.extension.psi.searchStaticField +import org.domaframework.doma.intellij.extension.psi.searchStaticMethod +import org.domaframework.doma.intellij.psi.SqlElAndExpr +import org.domaframework.doma.intellij.psi.SqlElClass +import org.domaframework.doma.intellij.psi.SqlElElseifDirective +import org.domaframework.doma.intellij.psi.SqlElEqExpr +import org.domaframework.doma.intellij.psi.SqlElFieldAccessExpr +import org.domaframework.doma.intellij.psi.SqlElForDirective +import org.domaframework.doma.intellij.psi.SqlElGeExpr +import org.domaframework.doma.intellij.psi.SqlElGtExpr +import org.domaframework.doma.intellij.psi.SqlElIfDirective +import org.domaframework.doma.intellij.psi.SqlElLeExpr +import org.domaframework.doma.intellij.psi.SqlElLtExpr +import org.domaframework.doma.intellij.psi.SqlElNeExpr +import org.domaframework.doma.intellij.psi.SqlElOrExpr +import org.domaframework.doma.intellij.psi.SqlElPrimaryExpr +import org.domaframework.doma.intellij.psi.SqlTypes +import org.jetbrains.kotlin.idea.base.util.module + +class SqlParameterCompletionProvider : CompletionProvider() { + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet, + ) { + val startTime = System.nanoTime() + + var isDirective = false + try { + val originalFile = parameters.originalFile + val project = originalFile.project + val pos = parameters.originalPosition ?: return + val bindText = + cleanString(pos.text) + .substringAfter("/*") + .substringBefore("*/") + + val handler = DirectiveCompletion(originalFile, bindText, pos, project, result) + val directiveSymbols = listOf("%", "#", "^", "@") + directiveSymbols.forEach { + if (!isDirective) { + isDirective = handler.directiveHandle(it) + if (isDirective) { + PluginLoggerUtil.countLogging( + this::class.java.simpleName, + "CompletionDirectiveBy$it", + "Completion", + startTime, + ) + return@forEach + } + } + } + + if (!isDirective) { + // Check when performing code completion on the right side + val prevElm = + pos.prevLeafs.firstOrNull { + it.isNotWhiteSpace() && + it.elementType != SqlTypes.EL_DOT && + it !is PsiErrorElement + } + if (!pos.isNotWhiteSpace() && !isRightFactor(prevElm)) return + + val blockElements = getAccessElementTextBlocks(parameters.originalPosition!!) + generateCompletionList( + blockElements, + originalFile, + result, + ) + } + } catch (e: Exception) { + e.printStackTrace() + } finally { + result.stopHere() + } + } + + /** + * Check to enable code completion even in the case of the right side + */ + private fun isRightFactor(prevElm: PsiElement?) = + ( + prevElm is SqlElEqExpr || + prevElm?.elementType == SqlTypes.EL_IDENTIFIER || + prevElm is SqlElGeExpr || + prevElm is SqlElGtExpr || + prevElm is SqlElLeExpr || + prevElm is SqlElLtExpr || + prevElm is SqlElNeExpr || + prevElm is SqlElOrExpr || + prevElm is SqlElAndExpr || + prevElm?.elementType == SqlTypes.EL_PLUS || + prevElm?.elementType == SqlTypes.EL_MINUS || + prevElm?.elementType == SqlTypes.EL_ASTERISK || + prevElm?.elementType == SqlTypes.EL_SLASH || + prevElm?.elementType == SqlTypes.EL_PERCENT || + prevElm?.isNotWhiteSpace()!! + ) + + private fun getAccessElementTextBlocks(targetElement: PsiElement): List { + var blocks: List = emptyList() + // If the immediate parent is a for, if, elseif directive, + // get the field access element list from its own forward element. + val parent = targetElement.parent + if (parent is SqlElForDirective || + parent is SqlElIfDirective || + parent is SqlElElseifDirective + ) { + val prevElms = + targetElement.findSelfBlocks() + if (prevElms.isNotEmpty()) { + return prevElms + } + } + // If the parent has field access, get its child element + if (targetElement.parent is SqlElFieldAccessExpr) { + blocks = + PsiTreeUtil + .getChildrenOfTypeAsList(targetElement.parent, PsiElement::class.java) + .filter { + ( + it is SqlElPrimaryExpr || + it.elementType == SqlTypes.EL_IDENTIFIER + ) && + it.parent !is SqlElClass + }.toList() + if (blocks.isEmpty()) { + val parent = + PsiTreeUtil.findFirstParent(targetElement.parent) { + it !is PsiDirectory && + it !is PsiFile && + it is SqlElFieldAccessExpr + } + + blocks = + PsiTreeUtil + .getChildrenOfTypeAsList(parent, PsiElement::class.java) + .filter { + it.elementType == SqlTypes.EL_IDENTIFIER && + (targetElement.startOffsetInParent) >= it.startOffsetInParent + }.toList() + } + } + // If the element has no parent-child relationship, + // create a list that also adds itself at the end. + if (blocks.isEmpty()) { + val prevElms = + targetElement.findSelfBlocks() + if (prevElms.isNotEmpty()) { + return prevElms + } + } + return blocks.sortedBy { it.textOffset } + } + + /** + * Determine the argument type from the input string, + * recursively obtain the Entity field, and return it as a code completion list. + */ + private fun generateCompletionList( + elements: List, + originalFile: PsiFile, + result: CompletionResultSet, + ) { + var topElementType: PsiType? = null + val top = + when { + elements.isEmpty() -> return + else -> elements.first() + } + val topText = cleanString(top.text) + val prevWord = PsiPatternUtil.getBindSearchWord(originalFile, elements.last(), " ") + if (prevWord.startsWith("@") && prevWord.endsWith("@")) { + val clazz = getRefClazz(top) { prevWord.replace("@", "") } ?: return + val matchFields = clazz.searchStaticField(topText) + val matchMethod = clazz.searchStaticMethod(topText) + + // When you enter here, it is the top element, so return static fields and methods. + setFieldsAndMethodsCompletionResultSet(matchFields, matchMethod, result) + return + } + if (top.parent !is PsiFile && top.parent.parent !is PsiDirectory) { + val staticDirective = top.findNodeParent(SqlTypes.EL_STATIC_FIELD_ACCESS_EXPR) + staticDirective?.let { + topElementType = getElementTypeByStaticFieldAccess(top, it, topText) ?: return + } + } + if (topElementType == null) { + topElementType = + getElementTypeByFieldAccess(originalFile, topText, elements, result) ?: return + } + + // If the name of the return value type is E, get the nested types in order from topElementType. + // Loop through the remaining child elements and generate a code completion list + // that displays by type of terminal element + var psiParentClass = PsiParentClass(topElementType) + var parentProperties: Array = psiParentClass.clazz?.allFields ?: emptyArray() + var parentMethods: Array = psiParentClass.clazz?.allMethods ?: emptyArray() + var index = 1 + var listParamIndex = 0 + for (elm in elements.drop(1)) { + index++ + val searchElm = cleanString(elm.text) + if (searchElm.isEmpty()) { + setFieldsAndMethodsCompletionResultSet( + (psiParentClass.searchField(searchElm)?.toTypedArray() ?: emptyArray()), + (psiParentClass.searchMethod(searchElm)?.toTypedArray() ?: emptyArray()), + result, + ) + return + } + + if (index < elements.size) { + psiParentClass.findField(searchElm)?.let { + if (psiParentClass.type != it.type) { + if (!PsiTypeChecker.isTargetType(it.type)) return + psiParentClass = PsiParentClass(it.type) + } + } + psiParentClass.findMethod(searchElm)?.let { + if (it.returnType == null) return + val listType = it.getMethodReturnType(topElementType, listParamIndex) ?: return + psiParentClass = PsiParentClass(listType) + } + listParamIndex++ + } else { + psiParentClass.searchField(searchElm)?.let { + parentProperties = it.toTypedArray() + } ?: { parentProperties = emptyArray() } + psiParentClass.searchMethod(searchElm)?.let { + parentMethods = it.toTypedArray() + } ?: { parentMethods = emptyArray() } + setFieldsAndMethodsCompletionResultSet(parentProperties, parentMethods, result) + } + } + } + + private fun getElementTypeByStaticFieldAccess( + top: PsiElement, + staticDirective: PsiElement, + topText: String, + ): PsiType? { + val clazz = + getRefClazz(top) { + staticDirective.children + .firstOrNull { it.elementType == SqlTypes.EL_CLASS } + ?.text + ?: "" + } ?: return null + + return clazz.findStaticField(topText)?.type + ?: clazz.findStaticMethod(topText)?.returnType + } + + private fun getElementTypeByFieldAccess( + originalFile: PsiFile, + topText: String, + elements: List, + result: CompletionResultSet, + ): PsiType? { + val daoMethod = findDaoMethod(originalFile) ?: return null + val matchParams = daoMethod.searchParameter(topText) + val firstElement = matchParams.firstOrNull() ?: return null + if (elements.isEmpty() || elements.size <= 1) { + matchParams.map { p -> + result.addElement(LookupElementBuilder.create(p.name)) + } + return null + } + val immediate = firstElement.getIterableClazz(daoMethod.getDomaAnnotationType()) + return immediate.type + } + + private fun getRefClazz( + top: PsiElement, + fqdnGetter: () -> String, + ): PsiClass? = top.module?.getJavaClazz(true, fqdnGetter()) + + private fun setFieldsAndMethodsCompletionResultSet( + fields: Array, + methods: Array, + result: CompletionResultSet, + ) { + result.addAllElements(fields.map { param -> VariableLookupItem(param) }) + methods.forEach { method -> + val lookupElm = + LookupElementBuilder + .create("${method.name}()") + .withPresentableText(method.name) + .withTailText(method.parameterList.text, true) + .withTypeText(method.returnType?.presentableText ?: "") + result.addElement(lookupElm) + } + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/extension/ModuleExtensions.kt b/src/main/kotlin/org/domaframework/doma/intellij/extension/ModuleExtensions.kt new file mode 100644 index 00000000..e2332841 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/extension/ModuleExtensions.kt @@ -0,0 +1,65 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.extension + +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ResourceFileUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiClass +import com.intellij.psi.search.GlobalSearchScope +import org.domaframework.doma.intellij.common.CommonPathParameter.Companion.RESOURCES_META_INF_PATH +import org.domaframework.doma.intellij.common.dao.formatSqlPathFromDaoPath +import org.jetbrains.kotlin.idea.util.sourceRoots + +/** + * Get SQL directory corresponding to Dao file + */ +fun Module.getPackagePathFromDaoPath(daoFile: VirtualFile): VirtualFile? { + val contentRoot = this.project.getContentRoot(daoFile)?.path + val packagePath = + contentRoot?.let { + formatSqlPathFromDaoPath(it, daoFile) + } ?: "" + + return this.getResourcesSQLFile( + packagePath, + false, + ) +} + +fun Module.getResourceRoot(): VirtualFile? = this.sourceRoots.firstOrNull { it.path.contains("/resources") } + +fun Module.getJavaClazz( + includeTest: Boolean, + fqdn: String, +): PsiClass? { + val scope = GlobalSearchScope.moduleRuntimeScope(this, includeTest) + return JavaPsiFacade + .getInstance(this.project) + .findClasses(fqdn, scope) + .firstOrNull() +} + +fun Module.getResourcesSQLFile( + relativePath: String, + includeTest: Boolean, +): VirtualFile? = + ResourceFileUtil.findResourceFileInScope( + "$RESOURCES_META_INF_PATH/${relativePath.replace(RESOURCES_META_INF_PATH,"")}".replace("//", "/"), + this.project, + this.getModuleScope(includeTest), + ) diff --git a/src/main/kotlin/org/domaframework/doma/intellij/extension/ProjectExtensions.kt b/src/main/kotlin/org/domaframework/doma/intellij/extension/ProjectExtensions.kt new file mode 100644 index 00000000..eb474ec5 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/extension/ProjectExtensions.kt @@ -0,0 +1,37 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.extension + +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ProjectRootManager +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiManager + +fun Project.getContentRoot(baseFile: VirtualFile): VirtualFile? = + ProjectRootManager + .getInstance(this) + .fileIndex + .getContentRootForFile(baseFile) + +fun Project.getModule(virtualFile: VirtualFile): Module? = + ProjectRootManager + .getInstance(this) + .fileIndex + .getModuleForFile(virtualFile) + +fun Project.findFile(file: VirtualFile): PsiFile? = PsiManager.getInstance(this).findFile(file) diff --git a/src/main/kotlin/org/domaframework/doma/intellij/extension/expr/SqlElExtensions.kt b/src/main/kotlin/org/domaframework/doma/intellij/extension/expr/SqlElExtensions.kt new file mode 100644 index 00000000..bed3ec52 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/extension/expr/SqlElExtensions.kt @@ -0,0 +1,45 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.extension.expr + +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.elementType +import org.domaframework.doma.intellij.psi.SqlElClass +import org.domaframework.doma.intellij.psi.SqlElPrimaryExpr +import org.domaframework.doma.intellij.psi.SqlElStaticFieldAccessExpr +import org.domaframework.doma.intellij.psi.SqlTypes + +val SqlElStaticFieldAccessExpr.accessElements: List + get() { + return PsiTreeUtil + .getChildrenOfType(this, PsiElement::class.java) + ?.filter { + ( + it.elementType == SqlTypes.EL_IDENTIFIER || + it is SqlElPrimaryExpr + ) + }?.sortedBy { it.textOffset } + ?.toList() + ?: emptyList() + } + +val SqlElStaticFieldAccessExpr.fqdn: String + get() { + val elClazz = PsiTreeUtil.getChildOfType(this, SqlElClass::class.java) ?: return "" + val fqdn = PsiTreeUtil.getChildrenOfTypeAsList(elClazz, PsiElement::class.java) + return fqdn.toList().joinToString("") { it.text } + } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/DomaAnnotationType.kt b/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/DomaAnnotationType.kt new file mode 100644 index 00000000..f53877f7 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/DomaAnnotationType.kt @@ -0,0 +1,62 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.extension.psi + +import com.intellij.codeInsight.AnnotationUtil +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiModifierListOwner + +enum class DomaAnnotationType( + val fqdn: String, + val extension: String = "sql", +) { + SqlProcessor("org.seasar.doma.SqlProcessor"), + Script("org.seasar.doma.Script", extension = "script"), + BatchInsert("org.seasar.doma.BatchInsert"), + BatchUpdate("org.seasar.doma.BatchUpdate"), + BatchDelete("org.seasar.doma.BatchDelete"), + Select("org.seasar.doma.Select"), + Insert("org.seasar.doma.Insert"), + Update("org.seasar.doma.Update"), + Delete("org.seasar.doma.Delete"), + Sql("org.seasar.doma.Sql"), + Unknown("Unknown"), + ; + + fun isBatchAnnotation(): Boolean = this == BatchInsert || this == BatchUpdate || this == BatchDelete + + fun isRequireSqlTemplate(): Boolean = this == Select || this == Script || this == SqlProcessor + + fun useSqlFileOption(): Boolean = + this == Insert || + this == Update || + this == Delete || + this == BatchInsert || + this == BatchUpdate || + this == BatchDelete + + fun getPsiAnnotation(element: PsiModifierListOwner): PsiAnnotation? = + AnnotationUtil.findAnnotation( + element, + this.fqdn, + ) + + fun getSqlFileVal(element: PsiAnnotation): Boolean = + when (this.useSqlFileOption()) { + true -> AnnotationUtil.getBooleanAttributeValue(element, "sqlFile") == true + false -> false + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiClassExtension.kt b/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiClassExtension.kt new file mode 100644 index 00000000..9906402d --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiClassExtension.kt @@ -0,0 +1,57 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.extension.psi + +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiField +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiModifier +import com.intellij.psi.util.PsiTypesUtil +import org.domaframework.doma.intellij.common.psi.PropertyModifyUtil + +val PsiClass.psiClassType: PsiClassType + get() = PsiTypesUtil.getClassType(this) + +fun PsiClass.searchStaticField(searchName: String): Array = + this.allFields + .filter { + it.name.startsWith(searchName) && + it.hasModifierProperty(PsiModifier.STATIC) && + PropertyModifyUtil.filterPrivateField(it, this.psiClassType) + }.toTypedArray() + +fun PsiClass.findStaticField(searchName: String): PsiField? = + this.allFields.firstOrNull { + it.name == searchName && + it.hasModifierProperty(PsiModifier.STATIC) && + PropertyModifyUtil.filterPrivateField(it, this.psiClassType) + } + +fun PsiClass.searchStaticMethod(searchName: String): Array = + this.allMethods + .filter { + it.name.startsWith(searchName) && + it.hasModifierProperty(PsiModifier.STATIC) && + it.hasModifierProperty(PsiModifier.PUBLIC) + }.toTypedArray() + +fun PsiClass.findStaticMethod(searchName: String): PsiMethod? = + this.allMethods.firstOrNull { + it.name == searchName && + it.hasModifierProperty(PsiModifier.STATIC) && + it.hasModifierProperty(PsiModifier.PUBLIC) + } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiElementExtension.kt b/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiElementExtension.kt new file mode 100644 index 00000000..6b256c56 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiElementExtension.kt @@ -0,0 +1,67 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.extension.psi + +import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiWhiteSpace +import com.intellij.psi.tree.IElementType +import com.intellij.psi.util.elementType +import com.intellij.psi.util.prevLeafs +import org.domaframework.doma.intellij.psi.SqlTypes + +fun PsiElement.isNotWhiteSpace(): Boolean = this !is PsiWhiteSpace + +// Get the list of elements before itself +fun PsiElement.findSelfBlocks(): List { + var elms = emptyList() + for (it in this.prevLeafs) { + elms = elms.plus(it) + if (!it.isNotWhiteSpace() || it.elementType == SqlTypes.EL_AT_SIGN) break + } + + elms + .filter { + it.elementType == SqlTypes.EL_PRIMARY_EXPR || + it.elementType == SqlTypes.EL_IDENTIFIER + }.toList() + .plus(this) + .also { elms = it } + if (elms.isNotEmpty()) { + return elms.sortedBy { it.textOffset } + } + return emptyList() +} + +/** + * Traverse a node's parent hierarchy + * to find the parent of the specified element type up to the comment block element + */ +fun PsiElement.findNodeParent(elementType: IElementType): PsiElement? { + var parentElm = this + while (parentElm.elementType != elementType && + parentElm.elementType != SqlTypes.BLOCK_COMMENT && + parentElm !is PsiFile && + parentElm !is PsiDirectory + ) { + parentElm = parentElm.parent + if (parentElm.elementType == elementType) { + return parentElm + } + } + return null +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiFileExtensions.kt b/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiFileExtensions.kt new file mode 100644 index 00000000..72460283 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiFileExtensions.kt @@ -0,0 +1,33 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.extension.psi + +import com.intellij.lang.injection.InjectedLanguageManager +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile + +/** + * For processing inside Sql annotations, get it as an injected custom language + */ +fun PsiFile.initPsiFileAndElement( + project: Project, + caretOffset: Int, +): PsiFile? { + val injectedLanguageManager = InjectedLanguageManager.getInstance(project) + val element = + injectedLanguageManager.findInjectedElementAt(this, caretOffset) ?: return null + return element.containingFile +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiMethodExtension.kt b/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiMethodExtension.kt new file mode 100644 index 00000000..5a8f17e4 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiMethodExtension.kt @@ -0,0 +1,66 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.extension.psi + +import com.intellij.codeInsight.AnnotationUtil +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiParameter +import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypeParameterList +import com.intellij.psi.impl.compiled.ClsClassImpl +import com.intellij.psi.impl.compiled.ClsTypeParametersListImpl +import com.intellij.psi.impl.source.PsiClassReferenceType + +fun PsiMethod.findParameter(searchName: String): PsiParameter? = this.methodParameters.firstOrNull { it.name == searchName } + +val PsiMethod.methodParameters: List + get() = this.parameterList.parameters.toList() + +fun PsiMethod.searchParameter(searchName: String): List = this.methodParameters.filter { it.name.startsWith(searchName) } + +fun PsiMethod.getDomaAnnotationType(): DomaAnnotationType { + DomaAnnotationType.entries.forEach { + if (AnnotationUtil.findAnnotation(this, it.fqdn) != null) { + return it + } + } + return DomaAnnotationType.Unknown +} + +/** + * If the type of the variable referenced from the Dao argument is List type, + * search processing to obtain the nested type + */ +fun PsiMethod.getMethodReturnType( + topElementType: PsiType, + index: Int, +): PsiClassType? { + val returnType = this.returnType as? PsiClassType + val cls = returnType?.resolve()?.parent as? ClsTypeParametersListImpl + val listType = ((cls as? PsiTypeParameterList)?.parent as? ClsClassImpl) + + if (returnType?.name == "E" && listType?.qualifiedName == "java.util.List") { + var count = 1 + var type: PsiType? = (topElementType as? PsiClassReferenceType)?.parameters?.firstOrNull() + while (index >= count && type != null && type is PsiClassReferenceType) { + type = type.parameters.firstOrNull() + count++ + } + return type as? PsiClassType + } + return returnType +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiParameterExtension.kt b/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiParameterExtension.kt new file mode 100644 index 00000000..45652dc8 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/PsiParameterExtension.kt @@ -0,0 +1,49 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.extension.psi + +import com.intellij.psi.PsiParameter +import com.intellij.psi.impl.source.PsiClassReferenceType +import org.domaframework.doma.intellij.common.psi.PsiParentClass + +/** + * For List type, if the annotation type is Batch type, + * return the content type. + */ +fun PsiParameter.getIterableClazz(annotationType: DomaAnnotationType): PsiParentClass { + val immediate: PsiClassReferenceType? = this.type as? PsiClassReferenceType + if (immediate != null) { + if (immediate.name == "List" && annotationType.isBatchAnnotation()) { + val listType = + (this.type as PsiClassReferenceType).parameters.firstOrNull() + return PsiParentClass(listType!!) + } + } + return PsiParentClass(this.type) +} + +val PsiParameter.isFunctionClazz: Boolean + get() = + (this.typeElement?.type as? PsiClassReferenceType) + ?.resolve() + ?.qualifiedName + ?.contains("java.util.function") == true + +val PsiParameter.isSelectOption: Boolean + get() = + (this.typeElement?.type as? PsiClassReferenceType) + ?.resolve() + ?.qualifiedName == "org.seasar.doma.jdbc.SelectOptions" diff --git a/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/SqlElForDirectiveExtension.kt b/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/SqlElForDirectiveExtension.kt new file mode 100644 index 00000000..a42ef1a3 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/extension/psi/SqlElForDirectiveExtension.kt @@ -0,0 +1,31 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.extension.psi + +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.elementType +import org.domaframework.doma.intellij.psi.SqlElForDirective +import org.domaframework.doma.intellij.psi.SqlElPrimaryExpr +import org.domaframework.doma.intellij.psi.SqlTypes + +fun SqlElForDirective.getForItem(): PsiElement? = + PsiTreeUtil + .getChildrenOfType(this, PsiElement::class.java) + ?.firstOrNull { + it.isNotWhiteSpace() && + (it is SqlElPrimaryExpr || it.elementType == SqlTypes.EL_IDENTIFIER) + } diff --git a/src/main/kotlin/org/domaframework/doma/intellij/gutter/dao/DaoMethodProvider.kt b/src/main/kotlin/org/domaframework/doma/intellij/gutter/dao/DaoMethodProvider.kt new file mode 100644 index 00000000..c8b7b816 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/gutter/dao/DaoMethodProvider.kt @@ -0,0 +1,101 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.gutter.dao + +import com.intellij.codeInsight.daemon.GutterIconNavigationHandler +import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo +import com.intellij.codeInsight.daemon.RelatedItemLineMarkerProvider +import com.intellij.navigation.GotoRelatedItem +import com.intellij.openapi.editor.markup.GutterIconRenderer +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiNameIdentifierOwner +import org.domaframework.doma.intellij.bundle.MessageBundle +import org.domaframework.doma.intellij.common.PluginLoggerUtil +import org.domaframework.doma.intellij.common.dao.getDaoClass +import org.domaframework.doma.intellij.common.dao.jumpSqlFromDao +import org.domaframework.doma.intellij.common.isJavaOrKotlinFileType +import org.domaframework.doma.intellij.common.psi.PsiDaoMethod +import org.domaframework.doma.intellij.setting.SqlIcon +import java.awt.event.MouseEvent +import javax.swing.Icon + +/** + * Line marker provider for DAO method + */ +class DaoMethodProvider : RelatedItemLineMarkerProvider() { + override fun getIcon(): Icon = SqlIcon.FILE + + override fun collectNavigationMarkers( + e: PsiElement, + result: MutableCollection>, + ) { + if (!isTargetElement(e)) return + + if (e.parent is PsiNameIdentifierOwner) { + val owner = e as PsiNameIdentifierOwner + val method = e as? PsiMethod ?: return + val identifier = owner.nameIdentifier ?: return + val psiDaoMethod = PsiDaoMethod(e.project, method) + if (!psiDaoMethod.isUseSqlFileMethod()) return + val target = psiDaoMethod.sqlFile ?: return + + val marker = + RelatedItemLineMarkerInfo( + identifier, + identifier.textRange, + icon, + { MessageBundle.message("jump.to.sql.tooltip.title") }, + getHandler(e.project, target), + GutterIconRenderer.Alignment.RIGHT, + ) { + ArrayList() + } + result.add(marker) + } + } + + private fun isTargetElement(e: PsiElement): Boolean { + if (!isJavaOrKotlinFileType(e.containingFile) && !isFunction(e)) { + return false + } + getDaoClass(e.containingFile) ?: return false + return e is PsiMethod + } + + /** + * whether the element is a function + * @return true if it is a function element + */ + private fun isFunction(element: PsiElement): Boolean = element is PsiMethod + + private fun getHandler( + project: Project, + file: VirtualFile, + ): GutterIconNavigationHandler = + GutterIconNavigationHandler { _: MouseEvent?, _: PsiElement? -> + val startTime = System.nanoTime() + jumpSqlFromDao(project, file) + PluginLoggerUtil.countLogging( + this::class.java.simpleName, + "JumpToSqlByGutter", + "Gutter", + startTime, + ) + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/gutter/sql/SqlLineMakerProvider.kt b/src/main/kotlin/org/domaframework/doma/intellij/gutter/sql/SqlLineMakerProvider.kt new file mode 100644 index 00000000..e2106269 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/gutter/sql/SqlLineMakerProvider.kt @@ -0,0 +1,109 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.gutter.sql + +import com.intellij.codeInsight.daemon.GutterIconNavigationHandler +import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo +import com.intellij.codeInsight.daemon.RelatedItemLineMarkerProvider +import com.intellij.icons.AllIcons +import com.intellij.navigation.GotoRelatedItem +import com.intellij.openapi.editor.markup.GutterIconRenderer +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import org.domaframework.doma.intellij.bundle.MessageBundle +import org.domaframework.doma.intellij.common.PluginLoggerUtil +import org.domaframework.doma.intellij.common.dao.findDaoFile +import org.domaframework.doma.intellij.common.dao.findDaoMethod +import org.domaframework.doma.intellij.common.dao.jumpToDaoMethod +import org.domaframework.doma.intellij.common.isSupportFileType +import org.jetbrains.kotlin.idea.core.util.toPsiFile +import java.awt.event.MouseEvent +import javax.swing.Icon + +/** + * Line marker provider for SQL file + */ +class SqlLineMakerProvider : RelatedItemLineMarkerProvider() { + override fun collectNavigationMarkers( + e: PsiElement, + result: MutableCollection>, + ) { + val project = e.project + val virtualFile = e.containingFile.virtualFile + if (!isSupportFileType(virtualFile)) return + // Display only on the first line + if (e.originalElement.parent.originalElement !is PsiFile || + e.textRange.startOffset != e.containingFile.textRange.startOffset + ) { + return + } + + val identifier = e.firstChild ?: e + val daoFile = + findDaoFile(project, virtualFile)?.let { + findDaoMethod(e.containingFile) ?: return + it + } ?: return + + val marker = + RelatedItemLineMarkerInfo( + identifier, + identifier.textRange, + getIcon(daoFile.toPsiFile(project)), + getToolTipTitle(daoFile.toPsiFile(project)), + getHandler(daoFile, identifier, virtualFile.nameWithoutExtension), + GutterIconRenderer.Alignment.RIGHT, + ) { + ArrayList() + } + result.add(marker) + } + + private fun getToolTipTitle(targetSqlFile: PsiFile?): ((Any) -> String) = + if (targetSqlFile != null) { + { MessageBundle.message("jump.to.dao.tooltip.title") } + } else { + { "" } + } + + private fun getIcon(targetSqlFile: PsiFile?): Icon { + if (targetSqlFile != null) { + return AllIcons.FileTypes.Java + } + return AllIcons.General.Error + } + + private fun getHandler( + targetDaoFile: VirtualFile?, + element: PsiElement, + methodName: String, + ): GutterIconNavigationHandler { + if (targetDaoFile != null) { + return GutterIconNavigationHandler { _: MouseEvent?, _: PsiElement? -> + val startTime = System.nanoTime() + jumpToDaoMethod(element.project, methodName, targetDaoFile) + PluginLoggerUtil.countLogging( + this::class.java.simpleName, + "JumpToDaoMethodByGutter", + "Gutter", + startTime, + ) + } + } + return GutterIconNavigationHandler { _: MouseEvent?, _: PsiElement? -> } + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/inspector/DaoMethodVariableInspector.kt b/src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/inspector/DaoMethodVariableInspector.kt new file mode 100644 index 00000000..7df8bdf5 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/inspector/DaoMethodVariableInspector.kt @@ -0,0 +1,113 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.inspection.dao.inspector + +import com.intellij.codeHighlighting.HighlightDisplayLevel +import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiParameter +import com.intellij.psi.PsiRecursiveElementVisitor +import com.intellij.psi.impl.source.PsiParameterImpl +import org.domaframework.doma.intellij.bundle.MessageBundle +import org.domaframework.doma.intellij.common.dao.getDaoClass +import org.domaframework.doma.intellij.common.isJavaOrKotlinFileType +import org.domaframework.doma.intellij.common.psi.PsiDaoMethod +import org.domaframework.doma.intellij.extension.findFile +import org.domaframework.doma.intellij.extension.psi.isFunctionClazz +import org.domaframework.doma.intellij.extension.psi.isSelectOption +import org.domaframework.doma.intellij.extension.psi.methodParameters +import org.domaframework.doma.intellij.psi.SqlElPrimaryExpr + +/** + * Check if Dao method arguments are used in the corresponding SQL file + */ +class DaoMethodVariableInspector : AbstractBaseJavaLocalInspectionTool() { + override fun getDisplayName(): String = "Method argument usage check" + + override fun getShortName(): String = "org.domaframework.doma.intellij.variablechecker" + + override fun getGroupDisplayName(): String = "DomaTools" + + override fun isEnabledByDefault(): Boolean = true + + override fun getDefaultLevel(): HighlightDisplayLevel = HighlightDisplayLevel.ERROR + + override fun buildVisitor( + holder: ProblemsHolder, + isOnTheFly: Boolean, + ): PsiElementVisitor { + return object : JavaElementVisitor() { + override fun visitMethod(method: PsiMethod) { + super.visitMethod(method) + val file = method.containingFile + if (!isJavaOrKotlinFileType(file)) return + getDaoClass(file) ?: return + + val psiDaoMethod = PsiDaoMethod(method.project, method) + if (!psiDaoMethod.useSqlAnnotation() && !psiDaoMethod.isUseSqlFileMethod()) return + + val methodParameters = + method.methodParameters + .filter { !it.isFunctionClazz && !it.isSelectOption } + val sqlFileManager = + psiDaoMethod.sqlFile?.let { + method.project.findFile(it) + } ?: return + + findElementsInSqlFile(sqlFileManager, methodParameters.toList()).forEach { arg -> + holder.registerProblem( + (arg.originalElement as PsiParameterImpl).nameIdentifier, + MessageBundle.message("inspection.dao.method.variable.error", arg.name), + ProblemHighlightType.ERROR, + ) + } + } + } + } + + fun findElementsInSqlFile( + sqlFile: PsiFile, + args: List, + ): List { + val elements = mutableListOf() + var iterator: Iterator + sqlFile.accept( + object : PsiRecursiveElementVisitor() { + // Recursively explore child elements in a file with PsiRecursiveElementVisitor. + override fun visitElement(element: PsiElement) { + if (element !is SqlElPrimaryExpr) { + iterator = args.minus(elements.toSet()).iterator() + while (iterator.hasNext()) { + val arg = iterator.next() + if (element.text == arg.name) { + elements.add(arg) + break + } + } + } + super.visitElement(element) + } + }, + ) + return args.minus(elements.toSet()) + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/inspector/SqlFileExistInspector.kt b/src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/inspector/SqlFileExistInspector.kt new file mode 100644 index 00000000..206452e5 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/inspector/SqlFileExistInspector.kt @@ -0,0 +1,77 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.inspection.dao.inspector + +import com.intellij.codeHighlighting.HighlightDisplayLevel +import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiMethod +import org.domaframework.doma.intellij.bundle.MessageBundle +import org.domaframework.doma.intellij.common.dao.getDaoClass +import org.domaframework.doma.intellij.common.isJavaOrKotlinFileType +import org.domaframework.doma.intellij.common.psi.PsiDaoMethod +import org.domaframework.doma.intellij.inspection.dao.quickfix.GenerateSQLFileQuickFixFactory + +/** + * Check for existence of SQL file + */ +class SqlFileExistInspector : AbstractBaseJavaLocalInspectionTool() { + override fun getDisplayName(): String = "Check for existence of SQL file" + + override fun getShortName(): String = "org.domaframework.doma.intellij.existsqlchecker" + + override fun getGroupDisplayName(): String = "DomaTools" + + override fun isEnabledByDefault(): Boolean = true + + override fun getDefaultLevel(): HighlightDisplayLevel = HighlightDisplayLevel.ERROR + + override fun buildVisitor( + holder: ProblemsHolder, + isOnTheFly: Boolean, + ): PsiElementVisitor { + return object : JavaElementVisitor() { + override fun visitMethod(method: PsiMethod) { + super.visitMethod(method) + val file = method.containingFile + if (!isJavaOrKotlinFileType(file)) return + getDaoClass(file) ?: return + + val psiDaoMethod = PsiDaoMethod(method.project, method) + if (psiDaoMethod.isUseSqlFileMethod()) { + checkDaoMethod(psiDaoMethod, holder) + } + } + } + } + + fun checkDaoMethod( + psiDaoMethod: PsiDaoMethod, + problemHolder: ProblemsHolder, + ) { + if (psiDaoMethod.sqlFile == null) { + problemHolder.registerProblem( + psiDaoMethod.psiMethod.nameIdentifier!!, + MessageBundle.message("inspection.sql.not.exist.error"), + ProblemHighlightType.ERROR, + GenerateSQLFileQuickFixFactory.createSql(psiDaoMethod), + ) + } + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/provider/DaoMethodVariableProvider.kt b/src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/provider/DaoMethodVariableProvider.kt new file mode 100644 index 00000000..08c98d7f --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/provider/DaoMethodVariableProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.inspection.dao.provider + +import com.intellij.codeInspection.InspectionToolProvider +import com.intellij.codeInspection.LocalInspectionTool +import org.domaframework.doma.intellij.inspection.dao.inspector.DaoMethodVariableInspector + +class DaoMethodVariableProvider : InspectionToolProvider { + override fun getInspectionClasses(): Array> = + arrayOf( + DaoMethodVariableInspector::class.java, + ) +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/provider/SqlFileExistProvider.kt b/src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/provider/SqlFileExistProvider.kt new file mode 100644 index 00000000..d6ff01aa --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/provider/SqlFileExistProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.inspection.dao.provider + +import com.intellij.codeInspection.InspectionToolProvider +import com.intellij.codeInspection.LocalInspectionTool +import org.domaframework.doma.intellij.inspection.dao.inspector.SqlFileExistInspector + +class SqlFileExistProvider : InspectionToolProvider { + override fun getInspectionClasses(): Array> = + arrayOf( + SqlFileExistInspector::class.java, + ) +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/quickfix/GenerateSqlFileQuickFixFactory.kt b/src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/quickfix/GenerateSqlFileQuickFixFactory.kt new file mode 100644 index 00000000..e2647c71 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/quickfix/GenerateSqlFileQuickFixFactory.kt @@ -0,0 +1,45 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +@file:Suppress("ktlint:standard:filename") +/* + * Copyright Doma Tools Authors + * + * 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. + */ + +package org.domaframework.doma.intellij.inspection.dao.quickfix + +import org.domaframework.doma.intellij.common.psi.PsiDaoMethod + +/** + * Inspect Dao and display quick fixes for problem areas + */ +object GenerateSQLFileQuickFixFactory { + /** + * Generate quick fix to generate SQL file corresponding to Dao method + */ + fun createSql(psiDaoMethod: PsiDaoMethod): GenerateSqlQuickFix = GenerateSqlQuickFix(psiDaoMethod) +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/quickfix/GenerateSqlQuickFix.kt b/src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/quickfix/GenerateSqlQuickFix.kt new file mode 100644 index 00000000..96144be2 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/inspection/dao/quickfix/GenerateSqlQuickFix.kt @@ -0,0 +1,48 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.inspection.dao.quickfix + +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.project.Project +import org.domaframework.doma.intellij.bundle.MessageBundle +import org.domaframework.doma.intellij.common.PluginLoggerUtil +import org.domaframework.doma.intellij.common.psi.PsiDaoMethod + +/** + * Quick fix to generate SQL files with Dao methods that require SQL templates + */ +class GenerateSqlQuickFix( + @Suppress("ActionIsNotPreviewFriendly") private val psiDaoMethod: PsiDaoMethod, +) : LocalQuickFix { + override fun getName(): String = familyName + + override fun getFamilyName(): String = MessageBundle.message("generate.sql.quickfix.title") + + override fun applyFix( + project: Project, + problemDescriptor: ProblemDescriptor, + ) { + val startTime = System.nanoTime() + psiDaoMethod.generateSqlFile() + PluginLoggerUtil.countLogging( + this::class.java.simpleName, + "GenerateSqlByQuickFix", + "QuickFix", + startTime, + ) + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/inspector/SqlBindVariableValidInspector.kt b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/inspector/SqlBindVariableValidInspector.kt new file mode 100644 index 00000000..bb3c8a12 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/inspector/SqlBindVariableValidInspector.kt @@ -0,0 +1,467 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.inspection.sql.inspector + +import com.intellij.codeHighlighting.HighlightDisplayLevel +import com.intellij.codeInspection.LocalInspectionTool +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.codeInspection.ex.ToolsImpl +import com.intellij.lang.injection.InjectedLanguageManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.profile.codeInspection.InspectionProfileManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiErrorElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiParameter +import com.intellij.psi.PsiType +import com.intellij.psi.impl.source.PsiClassReferenceType +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.elementType +import com.intellij.psi.util.nextLeafs +import org.domaframework.doma.intellij.bundle.MessageBundle +import org.domaframework.doma.intellij.common.dao.findDaoMethod +import org.domaframework.doma.intellij.common.isJavaOrKotlinFileType +import org.domaframework.doma.intellij.common.psi.PsiParentClass +import org.domaframework.doma.intellij.extension.expr.accessElements +import org.domaframework.doma.intellij.extension.expr.fqdn +import org.domaframework.doma.intellij.extension.getJavaClazz +import org.domaframework.doma.intellij.extension.psi.findNodeParent +import org.domaframework.doma.intellij.extension.psi.getDomaAnnotationType +import org.domaframework.doma.intellij.extension.psi.getForItem +import org.domaframework.doma.intellij.extension.psi.getIterableClazz +import org.domaframework.doma.intellij.extension.psi.getMethodReturnType +import org.domaframework.doma.intellij.extension.psi.methodParameters +import org.domaframework.doma.intellij.extension.psi.psiClassType +import org.domaframework.doma.intellij.psi.SqlElFieldAccessExpr +import org.domaframework.doma.intellij.psi.SqlElForDirective +import org.domaframework.doma.intellij.psi.SqlElNewExpr +import org.domaframework.doma.intellij.psi.SqlElParameters +import org.domaframework.doma.intellij.psi.SqlElPrimaryExpr +import org.domaframework.doma.intellij.psi.SqlElStaticFieldAccessExpr +import org.domaframework.doma.intellij.psi.SqlTypes +import org.domaframework.doma.intellij.psi.SqlVisitor +import org.jetbrains.kotlin.idea.base.util.module + +/** + * Code inspection for SQL bind variables + */ +class SqlBindVariableValidInspector : LocalInspectionTool() { + override fun getDisplayName(): String = "Match checking between SQL bind variables and Declaration" + + override fun getShortName(): String = "org.domaframework.doma.intellij.validBindVariable" + + override fun getGroupDisplayName(): String = "DomaTools" + + override fun isEnabledByDefault(): Boolean = true + + override fun getDefaultLevel(): HighlightDisplayLevel = HighlightDisplayLevel.Companion.ERROR + + data class BlockToken( + val type: BlockType, + val item: String, + val position: Int, + ) + + enum class BlockType { + FOR, + IF, + END, + } + + override fun buildVisitor( + holder: ProblemsHolder, + isOnTheFly: Boolean, + ): SqlVisitor { + val topElm = holder.file.firstChild + val directiveBlocks = + topElm.nextLeafs + .filter { elm -> + elm.elementType == SqlTypes.EL_FOR || + elm.elementType == SqlTypes.EL_IF || + elm.elementType == SqlTypes.EL_END + }.map { + when (it.elementType) { + SqlTypes.EL_FOR -> { + val item = (it.parent as? SqlElForDirective)?.getForItem() + BlockToken(BlockType.FOR, item?.text ?: "for", item?.textOffset ?: 0) + } + + SqlTypes.EL_IF -> BlockToken(BlockType.IF, "if", it.textOffset) + else -> BlockToken(BlockType.END, "end", it.textOffset) + } + } + + return object : SqlVisitor() { + override fun visitElStaticFieldAccessExpr(o: SqlElStaticFieldAccessExpr) { + super.visitElStaticFieldAccessExpr(o) + var file = o.containingFile ?: return + var targetElement = o + initInjectionElement(file, o.project, o.textOffset) + ?.let { + targetElement = it as SqlElStaticFieldAccessExpr + file = it.containingFile + } + checkStaticFieldAndMethodAccess(targetElement, holder) + } + + override fun visitElFieldAccessExpr(o: SqlElFieldAccessExpr) { + super.visitElFieldAccessExpr(o) + var file = o.containingFile ?: return + var targetElement = o + + initInjectionElement(file, o.project, o.textOffset) + ?.let { + targetElement = it as SqlElFieldAccessExpr + file = it.containingFile + } + + // Get element inside block comment + val blockElement = + PsiTreeUtil + .getChildrenOfTypeAsList(targetElement, PsiElement::class.java) + .filter { + it.elementType != SqlTypes.EL_DOT && + it.elementType != SqlTypes.EL_LEFT_PAREN && + it.elementType != SqlTypes.EL_RIGHT_PAREN && + it.elementType != SqlTypes.EL_PARAMETERS + }.toTypedArray() + val topElm = blockElement.firstOrNull() as SqlElPrimaryExpr + + // Exclude fixed Literal + if (isLiteralOrStatic(topElm)) return + // If inside a For block, search for IDENTIFY elements matching itself inside %for + if (checkInForDirectiveBlock(topElm)) return + checkAccessFieldAndMethod(holder, blockElement, file) + } + + override fun visitElPrimaryExpr(o: SqlElPrimaryExpr) { + super.visitElPrimaryExpr(o) + var file = o.containingFile ?: return + var targetElement = o + val project = o.project + initInjectionElement(file, o.project, o.textOffset) + ?.let { + targetElement = it as SqlElPrimaryExpr + file = it.containingFile + } + + // Exclude fixed Literal + if (isLiteralOrStatic(targetElement)) return + + // TODO Check function parameters in the same way as bind variables. + if (targetElement.findNodeParent(SqlTypes.EL_PARAMETERS) != null) return + + // For static property references, match against properties in the class definition + if (targetElement.parent is SqlElStaticFieldAccessExpr) { + checkStaticFieldAndMethodAccess( + targetElement.parent as SqlElStaticFieldAccessExpr, + holder, + ) + return + } + if (checkInForDirectiveBlock(targetElement)) return + val identify = + PsiTreeUtil + .getChildrenOfType(targetElement, PsiElement::class.java) + ?.firstOrNull() ?: return + val daoMethod = findDaoMethod(file) ?: return + val params = daoMethod.methodParameters + val validDaoParam = + params.firstOrNull { p -> + p.name == identify.text + } + + if (validDaoParam == null) { + val highlightElm = identify.originalElement ?: return + val highlightRange = + TextRange(0, identify.textRange?.length ?: 0) + setHighlightDaoMethodBind( + highlightRange, + highlightElm, + holder, + daoMethod, + project, + ) + } + } + + /** + * For processing inside Sql annotations, get it as an injected custom language + */ + private fun initInjectionElement( + basePsiFile: PsiFile, + project: Project, + caretOffset: Int, + ): PsiElement? = + when (isJavaOrKotlinFileType(basePsiFile)) { + true -> + { + val injectedLanguageManager = InjectedLanguageManager.getInstance(project) + injectedLanguageManager.findInjectedElementAt(basePsiFile, caretOffset) + } + + false -> null + } + + private fun checkInForDirectiveBlock(targetElement: PsiElement): Boolean { + val forBlocks = getForDirectiveBlock(targetElement) + val forItemNames = forBlocks.map { it.item } + val targetName = + targetElement.text + .replace("_has_next", "") + .replace("_index", "") + return forItemNames.contains(targetName) + } + + /** + * Count the `for`, `if`, and `end` elements from the beginning + * to the target element (`targetElement`) + * and obtain the `for` block information to which the `targetElement` belongs. + */ + fun getForDirectiveBlock(targetElement: PsiElement): List { + val preBlocks = + directiveBlocks + .filter { it.position <= targetElement.textOffset } + val stack = mutableListOf() + preBlocks.forEach { block -> + when (block.type) { + BlockType.FOR, BlockType.IF -> stack.add(block) + BlockType.END -> if (stack.isNotEmpty()) stack.removeAt(stack.lastIndex) + } + } + + return stack.filter { it.type == BlockType.FOR } + } + + private fun isLiteralOrStatic(targetElement: PsiElement): Boolean = + ( + targetElement.firstChild?.elementType == SqlTypes.EL_STRING || + targetElement.firstChild?.elementType == SqlTypes.EL_NUMBER || + targetElement.firstChild?.elementType == SqlTypes.EL_NULL || + targetElement.firstChild?.elementType == SqlTypes.EL_BOOLEAN || + targetElement.firstChild is SqlElNewExpr || + targetElement.text.startsWith("@") + ) + + private fun checkAccessFieldAndMethod( + holder: ProblemsHolder, + blockElement: Array, + file: PsiFile, + ) { + val topElement: PsiElement = blockElement.firstOrNull() ?: return + + val daoMethod = findDaoMethod(file) ?: return + val params = daoMethod.methodParameters + val validDaoParam = + params.firstOrNull { p -> + p.name == topElement.text + } + + // Check the match with the Dao method argument of the top element + // Do not perform subsequent processing if it is a fixed character or non-existent variable name + val validTopElement = + validateDaoParam(topElement, validDaoParam, holder, daoMethod) + if (validTopElement && validDaoParam != null) { + val parentClass = + validDaoParam.getIterableClazz(daoMethod.getDomaAnnotationType()) + inspectElements( + blockElement.toList(), + parentClass, + parentClass.type, + holder, + ) + } + } + + /** + * Check for existence of static field + */ + private fun checkStaticFieldAndMethodAccess( + staticAccuser: SqlElStaticFieldAccessExpr, + holder: ProblemsHolder, + ) { + val blockElements = staticAccuser.accessElements + val staticTopElement = blockElements.firstOrNull() ?: return + val module = staticAccuser.module ?: return + val fqdn = staticAccuser.fqdn + val clazz = module.getJavaClazz(false, fqdn) ?: return + + var parent = PsiParentClass(clazz.psiClassType) + val topField = + if (staticTopElement.nextSibling !is SqlElParameters) { + parent.findStaticField(staticTopElement.text) + } else { + null + } + val topMethod = + if (staticTopElement.nextSibling is SqlElParameters) { + parent.findStaticMethod(staticTopElement.text) + } else { + null + } + if (topField == null && topMethod == null) { + highlightElement(staticTopElement, holder, parent) + return + } + + (topField?.type ?: topMethod?.returnType) + ?.let { parent = PsiParentClass(it) } + ?: return + + val topElementType = parent.type + inspectElements(blockElements, parent, topElementType, holder) + } + + private fun highlightElement( + element: PsiElement, + holder: ProblemsHolder, + parent: PsiParentClass, + ) { + if (element is PsiErrorElement) return + val project = element.project + val highlightElm = element.originalElement + val highlightRange = + TextRange(0, element.textRange.length) + setHighlightParameterBind( + highlightRange, + highlightElm, + holder, + parent.clazz?.name ?: (parent.type as? PsiClassReferenceType)?.name, + project, + ) + } + + fun inspectElements( + blockElements: List, + topParent: PsiParentClass, + topElementType: PsiType, + holder: ProblemsHolder, + ) { + var parent = topParent + var listTypeSearchIndex = 0 + for (eml in blockElements.drop(1)) { + var isExist = false + parent + .findField(eml.text) + ?.let { match -> + isExist = true + parent = PsiParentClass(match.type) + } + if (isExist) continue + parent + .findMethod(eml.text) + ?.let { match -> + val returnType = + match.getMethodReturnType(topElementType, listTypeSearchIndex) + ?: return + isExist = true + parent = PsiParentClass(returnType) + } + listTypeSearchIndex++ + if (!isExist) { + highlightElement(eml, holder, parent) + return + } + } + } + + /** + * Verify element top matches Dao method argument + * Even if the name is not in the argument, a specific string will not cause an error. + * @return present in the Dao argument or declared as a for block item + */ + private fun validateDaoParam( + element: PsiElement, + validDaoParam: PsiParameter?, + holder: ProblemsHolder, + daoMethod: PsiMethod, + ): Boolean { + if (validDaoParam == null) { + if (checkInForDirectiveBlock(element)) return true + val highlightRange = TextRange(0, element.textRange.length) + setHighlightDaoMethodBind(highlightRange, element, holder, daoMethod, element.project) + return false + } + return true + } + } + } + + private fun setHighlightDaoMethodBind( + highlightRange: TextRange, + identify: PsiElement, + holder: ProblemsHolder, + daoMethod: PsiMethod, + project: Project, + ) { + holder.registerProblem( + identify, + MessageBundle.message( + "inspector.invalid.dao.parameter", + daoMethod.name, + identify.text ?: "", + ), + problemHighlightType(project, this.shortName), + highlightRange, + ) + } + + private fun setHighlightParameterBind( + highlightRange: TextRange, + identify: PsiElement, + holder: ProblemsHolder, + parentClassName: String?, + project: Project, + ) { + holder.registerProblem( + identify, + MessageBundle.message( + "inspector.invalid.class.property", + parentClassName ?: "", + identify.text ?: "", + ), + problemHighlightType(project, this.shortName), + highlightRange, + ) + } + + private fun getInspectionErrorLevel( + project: Project, + inspectionShortName: String, + ): HighlightDisplayLevel? { + val profileManager = InspectionProfileManager.getInstance(project) + val currentProfile = profileManager.currentProfile + val toolState: ToolsImpl? = currentProfile.getToolsOrNull(inspectionShortName, project) + return toolState?.level + } + + private fun problemHighlightType( + project: Project, + shortName: String, + ) = when ( + getInspectionErrorLevel( + project, + shortName, + ) + ) { + HighlightDisplayLevel.Companion.ERROR -> ProblemHighlightType.ERROR + HighlightDisplayLevel.Companion.WARNING -> ProblemHighlightType.WARNING + else -> ProblemHighlightType.WARNING + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/provider/SqlBindVariableValidProvider.kt b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/provider/SqlBindVariableValidProvider.kt new file mode 100644 index 00000000..acd87f45 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/inspection/sql/provider/SqlBindVariableValidProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.inspection.sql.provider + +import com.intellij.codeInspection.InspectionToolProvider +import com.intellij.codeInspection.LocalInspectionTool +import org.domaframework.doma.intellij.inspection.sql.inspector.SqlBindVariableValidInspector + +class SqlBindVariableValidProvider : InspectionToolProvider { + override fun getInspectionClasses(): Array> = + arrayOf( + SqlBindVariableValidInspector::class.java, + ) +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/refactoring/dao/DaoMethodRenameProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/refactoring/dao/DaoMethodRenameProcessor.kt new file mode 100644 index 00000000..279ea477 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/refactoring/dao/DaoMethodRenameProcessor.kt @@ -0,0 +1,64 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.refactoring.dao + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import com.intellij.refactoring.listeners.RefactoringElementListener +import com.intellij.refactoring.rename.RenameJavaMethodProcessor +import com.intellij.usageView.UsageInfo +import org.domaframework.doma.intellij.common.PluginLoggerUtil +import org.domaframework.doma.intellij.common.psi.PsiDaoMethod + +/** + * Rename DAO method + */ +class DaoMethodRenameProcessor : RenameJavaMethodProcessor() { + // Target classes with @Select and no @Sql, or with sqlFile=true + override fun canProcessElement(element: PsiElement): Boolean { + if (super.canProcessElement(element) && element is PsiMethod) { + val psiDaoMethod = PsiDaoMethod(element.project, element) + return psiDaoMethod.isUseSqlFileMethod() + } + return false + } + + override fun renameElement( + element: PsiElement, + newName: String, + usages: Array, + listener: RefactoringElementListener?, + ) { + val startTime = System.nanoTime() + if (element is PsiMethod) { + val psiDaoMethod = PsiDaoMethod(element.project, element) + val sqlExtension = psiDaoMethod.daoType.extension + val sqlFile = psiDaoMethod.sqlFile + if (sqlFile != null) { + if (sqlFile.nameWithoutExtension == element.name) { + sqlFile.rename(sqlFile, "$newName.$sqlExtension") + } + } + } + super.renameElement(element, newName, usages, listener) + PluginLoggerUtil.countLogging( + this::class.java.simpleName, + "RenameDaoMethod", + "Rename", + startTime, + ) + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/refactoring/dao/DaoPackageRenameListenerProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/refactoring/dao/DaoPackageRenameListenerProcessor.kt new file mode 100644 index 00000000..d379b6da --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/refactoring/dao/DaoPackageRenameListenerProcessor.kt @@ -0,0 +1,190 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.refactoring.dao + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.psi.JavaDirectoryService +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiJavaFile +import com.intellij.psi.PsiPackage +import com.intellij.refactoring.listeners.RefactoringElementListener +import com.intellij.refactoring.listeners.RefactoringElementListenerProvider +import com.intellij.util.IncorrectOperationException +import org.domaframework.doma.intellij.common.CommonPathParameter.Companion.RESOURCES_META_INF_PATH +import org.domaframework.doma.intellij.common.PluginLoggerUtil +import org.domaframework.doma.intellij.common.dao.getDaoClass +import org.domaframework.doma.intellij.extension.getResourceRoot +import org.domaframework.doma.intellij.extension.getResourcesSQLFile +import org.jetbrains.kotlin.idea.base.util.module +import java.io.IOException + +/** + * SQL directory factoring + * Perform SQL directory rename processing after standard refactoring + */ +class DaoPackageRenameListenerProcessor : RefactoringElementListenerProvider { + override fun getListener(element: PsiElement): RefactoringElementListener? { + if (!isPackageElement(element)) return null + val startTime = System.nanoTime() + val clazzName = this::class.java.simpleName + val psiPackage = getPsiPackage(element) ?: return null + + val oldQualifiedName = psiPackage.qualifiedName + val module = getModule(element) ?: return null + + return object : RefactoringElementListener { + override fun elementMoved(newelement: PsiElement) { + if (newelement is PsiClass) { + if (getDaoClass(newelement.containingFile) == null) return + refactoringMoveClassFile(module, newelement) + } else { + refactoringPackageRenameOrMove(module, newelement) + } + } + + override fun elementRenamed(newelement: PsiElement) { + if (newelement is PsiClass) return + refactoringPackageRenameOrMove(module, newelement) + } + + private fun refactoringPackageRenameOrMove( + module: Module, + newElement: PsiElement, + ) { + val psiPackage = getPsiPackage(newElement) ?: return + val directories = + psiPackage.directories + .filter { !it.name.contains("build") } + directories.forEach { dir -> + handlePackageMove(psiPackage, oldQualifiedName, module) + } + + PluginLoggerUtil.countLogging( + clazzName, + "RenamePackage", + "Rename", + startTime, + ) + } + + private fun refactoringMoveClassFile( + module: Module, + newElement: PsiElement, + ) { + val psiPackage = getPsiPackage(newElement) ?: return + val moveFileName = newElement.containingFile?.virtualFile?.nameWithoutExtension + val directories = + psiPackage.directories + .filter { !it.name.contains("build") } + directories.forEach { dir -> + handlePackageMove(psiPackage, oldQualifiedName, module, moveFileName) + } + + PluginLoggerUtil.countLogging( + clazzName, + "MoveFile", + "Move", + startTime, + ) + } + + private fun handlePackageMove( + packageElement: PsiPackage, + oldQualifiedName: String, + module: Module, + moveFileName: String? = null, + ) { + val newQualifiedName = packageElement.qualifiedName + renameOrMovePackage( + module, + oldQualifiedName, + newQualifiedName, + moveFileName, + ) + } + + private fun renameOrMovePackage( + module: Module, + oldQualifiedName: String, + newQualifiedName: String, + moveFileName: String? = null, + ) { + val baseDir = "${module.getResourceRoot()?.path}/$RESOURCES_META_INF_PATH/" + val newPath = "$baseDir/${newQualifiedName.replace(".", "/")}" + + ApplicationManager.getApplication().runWriteAction { + try { + val newDir = VfsUtil.createDirectories(newPath) + val oldResourcePath = module.getResourcesSQLFile(oldQualifiedName.replace(".", "/"), false) + oldResourcePath?.children?.forEach { old -> + if (moveFileName != null && old.name == moveFileName) { + old?.move(this, newDir) + } else if (moveFileName == null) { + old?.move(this, newDir) + } + } + } catch (e: IOException) { + when (e) { + is FileSystemException -> { + e.printStackTrace() + } + + else -> throw IncorrectOperationException(e) + } + } + } + } + } + } + + private fun getPsiPackage(newElement: PsiElement): PsiPackage? = + when (newElement) { + is PsiPackage -> newElement + is PsiClass, is PsiJavaFile -> + JavaDirectoryService + .getInstance() + .getPackage(newElement.containingFile.containingDirectory) + + else -> null + } + + private fun getModule(element: PsiElement): Module? = + when (element) { + is PsiPackage -> { + val modules = element.directories.map { it.module }.toSet() + if (modules.size == 1) { + modules.first() + } else { + ModuleUtilCore.findModuleForPsiElement(element) + } + } + + is PsiClass, is PsiJavaFile -> { + ModuleUtilCore.findModuleForFile(element.containingFile) + } + + is PsiDirectory -> element.module + else -> null + } + + private fun isPackageElement(element: PsiElement): Boolean = + element is PsiPackage || element is PsiClass || element is PsiJavaFile || element is PsiDirectory +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/refactoring/dao/DaoRenameProcessor.kt b/src/main/kotlin/org/domaframework/doma/intellij/refactoring/dao/DaoRenameProcessor.kt new file mode 100644 index 00000000..2b9e5a2d --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/refactoring/dao/DaoRenameProcessor.kt @@ -0,0 +1,61 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.refactoring.dao + +import com.intellij.psi.PsiElement +import com.intellij.refactoring.listeners.RefactoringElementListener +import com.intellij.refactoring.rename.RenameJavaClassProcessor +import com.intellij.usageView.UsageInfo +import org.domaframework.doma.intellij.common.PluginLoggerUtil +import org.domaframework.doma.intellij.common.dao.getDaoClass +import org.domaframework.doma.intellij.extension.getContentRoot +import org.domaframework.doma.intellij.extension.getPackagePathFromDaoPath +import org.jetbrains.kotlin.idea.base.util.module + +/** + * Rename DAO class + */ +class DaoRenameProcessor : RenameJavaClassProcessor() { + override fun canProcessElement(element: PsiElement): Boolean = + super.canProcessElement(element) && getDaoClass(element.containingFile) != null + + override fun renameElement( + element: PsiElement, + newName: String, + usages: Array, + listener: RefactoringElementListener?, + ) { + val startTime = System.nanoTime() + val daoClass = getDaoClass(element.containingFile) ?: return + + val project = element.project + val virtualFile = element.containingFile.virtualFile + project.getContentRoot(virtualFile)?.let { + element.module?.getPackagePathFromDaoPath(virtualFile)?.let { + if (it.name == daoClass.name) { + it.rename(it, newName) + } + } + } + super.renameElement(element, newName, usages, listener) + PluginLoggerUtil.countLogging( + this::class.java.simpleName, + "RenameClass", + "Rename", + startTime, + ) + } +} diff --git a/src/main/kotlin/org/domaframework/doma/intellij/setting/DomaToolStartupActivity.kt b/src/main/kotlin/org/domaframework/doma/intellij/setting/DomaToolStartupActivity.kt new file mode 100644 index 00000000..af1693b5 --- /dev/null +++ b/src/main/kotlin/org/domaframework/doma/intellij/setting/DomaToolStartupActivity.kt @@ -0,0 +1,33 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.setting + +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity +import org.domaframework.doma.intellij.common.PluginUtil + +class DomaToolStartupActivity : ProjectActivity { + override suspend fun execute(project: Project) { + System.setProperty("org.domaframework.doma.intellij.log.path", PathManager.getLogPath()) + System.setProperty( + "org.domaframework.doma.intellij.plugin.version", + PluginUtil.getVersion(), + ) + + println("PluginVersion: ${System.getProperty("org.domaframework.doma.intellij.plugin.version")} ") + } +} diff --git a/src/main/resources/META-INF/injections.xml b/src/main/resources/META-INF/injections.xml new file mode 100644 index 00000000..721a849b --- /dev/null +++ b/src/main/resources/META-INF/injections.xml @@ -0,0 +1,10 @@ + + + + Sql.value (org.seasar.doma) + Inject @Sql value as SQL + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/kotlin.xml b/src/main/resources/META-INF/kotlin.xml new file mode 100644 index 00000000..31c54bb0 --- /dev/null +++ b/src/main/resources/META-INF/kotlin.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml new file mode 100644 index 00000000..578a7e2b --- /dev/null +++ b/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,104 @@ + + + org.domaframework.doma + Doma Tools + domaframework + + com.intellij.modules.platform + com.intellij.modules.lang + com.intellij.modules.java + org.intellij.intelliLang + org.toml.lang + org.jetbrains.kotlin + + + messages.DomaToolsBundle + messages.LLMInstallerBundle + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/icons/SqlFile.svg b/src/main/resources/icons/SqlFile.svg new file mode 100644 index 00000000..3e463598 --- /dev/null +++ b/src/main/resources/icons/SqlFile.svg @@ -0,0 +1,17 @@ + + + + SQL + diff --git a/src/main/resources/inspectionDescriptions/org.domaframework.doma.intellij.existsqlchecker.html b/src/main/resources/inspectionDescriptions/org.domaframework.doma.intellij.existsqlchecker.html new file mode 100644 index 00000000..ac624f32 --- /dev/null +++ b/src/main/resources/inspectionDescriptions/org.domaframework.doma.intellij.existsqlchecker.html @@ -0,0 +1,9 @@ + + +

+ Dao method inspection by Doma Tools. + Displays warnings or errors for missing method definitions in SQL files. + A quick fix for SQL generation is suggested for the target. +

+ + \ No newline at end of file diff --git a/src/main/resources/inspectionDescriptions/org.domaframework.doma.intellij.validBindVariable.html b/src/main/resources/inspectionDescriptions/org.domaframework.doma.intellij.validBindVariable.html new file mode 100644 index 00000000..034c2262 --- /dev/null +++ b/src/main/resources/inspectionDescriptions/org.domaframework.doma.intellij.validBindVariable.html @@ -0,0 +1,7 @@ + + +

+ Doma Tools checks whether bind variables used in SQL exist in Dao method arguments or classes. +

+ + \ No newline at end of file diff --git a/src/main/resources/inspectionDescriptions/org.domaframework.doma.intellij.variablechecker.html b/src/main/resources/inspectionDescriptions/org.domaframework.doma.intellij.variablechecker.html new file mode 100644 index 00000000..6d22015f --- /dev/null +++ b/src/main/resources/inspectionDescriptions/org.domaframework.doma.intellij.variablechecker.html @@ -0,0 +1,7 @@ + + +

+ Doma Tools displays warnings or errors for unused method arguments in SQL +

+ + \ No newline at end of file diff --git a/src/main/resources/logback-test.xml b/src/main/resources/logback-test.xml new file mode 100644 index 00000000..7c99088d --- /dev/null +++ b/src/main/resources/logback-test.xml @@ -0,0 +1,35 @@ + + + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + UTF-8 + + + + + ${LOG_FILE_BASE_DIR:-/domatoolslog}/${LOG_FILE_BASENAME:-doma-tools}-${LOG_FILE_VERSION:-0.3.0}.log + + ${LOG_FILE_BASENAME:-doma-tools}-${LOG_FILE_VERSION:-0.3.0}-%d{yyyy-MM-dd}.log + 30 + + + "%date",%msg%n + UTF-8 + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 00000000..7c99088d --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,35 @@ + + + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + UTF-8 + + + + + ${LOG_FILE_BASE_DIR:-/domatoolslog}/${LOG_FILE_BASENAME:-doma-tools}-${LOG_FILE_VERSION:-0.3.0}.log + + ${LOG_FILE_BASENAME:-doma-tools}-${LOG_FILE_VERSION:-0.3.0}-%d{yyyy-MM-dd}.log + 30 + + + "%date",%msg%n + UTF-8 + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/messages/DomaToolsBundle.properties b/src/main/resources/messages/DomaToolsBundle.properties new file mode 100644 index 00000000..cf407d8b --- /dev/null +++ b/src/main/resources/messages/DomaToolsBundle.properties @@ -0,0 +1,7 @@ +jump.to.sql.tooltip.title=Open SQL file +jump.to.dao.tooltip.title=Jump to Dao method definition +generate.sql.quickfix.title=Create SQL file +inspection.sql.not.exist.error=SQL file does not exist +inspector.invalid.class.property=The field or method [{1}] does not exist in the class [{0}] +inspection.dao.method.variable.error=There are unused parameters in the SQL [{0}] +inspector.invalid.dao.parameter=The bind variable [{1}] does not exist in the Dao method [{0}] \ No newline at end of file diff --git a/src/main/resources/messages/DomaToolsBundle_ja.properties b/src/main/resources/messages/DomaToolsBundle_ja.properties new file mode 100644 index 00000000..eeecfc6c --- /dev/null +++ b/src/main/resources/messages/DomaToolsBundle_ja.properties @@ -0,0 +1,7 @@ +jump.to.sql.tooltip.title=SQL\u30D5\u30A1\u30A4\u30EB\u3092\u958B\u304F +jump.to.dao.tooltip.title=Dao\u30E1\u30BD\u30C3\u30C9\u5B9A\u7FA9\u306B\u9077\u79FB\u3059\u308B +generate.sql.quickfix.title=SQL\u30D5\u30A1\u30A4\u30EB\u3092\u4F5C\u6210 +inspection.sql.not.exist.error=SQL\u30D5\u30A1\u30A4\u30EB\u304C\u5B58\u5728\u3057\u307E\u305B\u3093 +inspector.invalid.class.property=\u30AF\u30E9\u30B9[{0}]\u306B\u5B58\u5728\u3057\u306A\u3044\u30D5\u30A3\u30FC\u30EB\u30C9\u3001\u307E\u305F\u306F\u30E1\u30BD\u30C3\u30C9\u304C\u4F7F\u7528\u3055\u308C\u3066\u3044\u307E\u3059:[{1}] +inspection.dao.method.variable.error=SQL\u3067\u4F7F\u7528\u3055\u308C\u3066\u3044\u306A\u3044\u5F15\u6570\u304C\u3042\u308A\u307E\u3059[{0}] +inspector.invalid.dao.parameter=Dao\u30E1\u30BD\u30C3\u30C9[{0}]\u306B\u5B58\u5728\u3057\u306A\u3044\u30D0\u30A4\u30F3\u30C9\u5909\u6570\u304C\u4F7F\u7528\u3055\u308C\u3066\u3044\u307E\u3059:[{1}] \ No newline at end of file diff --git a/src/main/resources/messages/LLMInstallerBundle.properties b/src/main/resources/messages/LLMInstallerBundle.properties new file mode 100644 index 00000000..6b3a8d04 --- /dev/null +++ b/src/main/resources/messages/LLMInstallerBundle.properties @@ -0,0 +1 @@ +ai.installer.notification.group.name = AI Installer \ No newline at end of file diff --git a/src/main/resources/messages/LLMInstallerBundle_en.properties b/src/main/resources/messages/LLMInstallerBundle_en.properties new file mode 100644 index 00000000..e69de29b diff --git a/src/main/resources/messages/LLMInstallerBundle_ja.properties b/src/main/resources/messages/LLMInstallerBundle_ja.properties new file mode 100644 index 00000000..e69de29b diff --git a/src/test/kotlin/org/domaframework/doma/intellij/DomaProjectDiscriptor.kt b/src/test/kotlin/org/domaframework/doma/intellij/DomaProjectDiscriptor.kt new file mode 100644 index 00000000..00905cb6 --- /dev/null +++ b/src/test/kotlin/org/domaframework/doma/intellij/DomaProjectDiscriptor.kt @@ -0,0 +1,53 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +@file:Suppress("ktlint:standard:filename") +/* + * Copyright Doma Tools Authors + * + * 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. + */ + +package org.domaframework.doma.intellij + +import com.intellij.openapi.module.Module +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.roots.ContentEntry +import com.intellij.openapi.roots.ModifiableRootModel +import com.intellij.pom.java.LanguageLevel +import com.intellij.testFramework.IdeaTestUtil +import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor + +class DomaProjectDescriptor : DefaultLightProjectDescriptor() { + override fun getSdk(): Sdk = IdeaTestUtil.getMockJdk17() + + override fun configureModule( + module: Module, + model: ModifiableRootModel, + contentEntry: ContentEntry, + ) { + IdeaTestUtil.setModuleLanguageLevel(module, LanguageLevel.JDK_17) + } +} diff --git a/src/test/kotlin/org/domaframework/doma/intellij/DomaSqlTest.kt b/src/test/kotlin/org/domaframework/doma/intellij/DomaSqlTest.kt new file mode 100644 index 00000000..e7713006 --- /dev/null +++ b/src/test/kotlin/org/domaframework/doma/intellij/DomaSqlTest.kt @@ -0,0 +1,183 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij + +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.module.Module +import com.intellij.openapi.projectRoots.JavaSdk +import com.intellij.openapi.projectRoots.ProjectJdkTable +import com.intellij.openapi.roots.ModifiableRootModel +import com.intellij.openapi.roots.ModuleRootModificationUtil +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiClass +import com.intellij.testFramework.PsiTestUtil +import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase +import org.domaframework.doma.intellij.extension.getResourcesSQLFile +import org.jetbrains.jps.model.java.JavaResourceRootType +import org.jetbrains.jps.model.java.JavaSourceRootType +import org.junit.Ignore +import java.io.File + +@Ignore +open class DomaSqlTest : LightJavaCodeInsightFixtureTestCase() { + protected val packagePath = "doma/example" + private val resourceRoot = "main/resources" + + override fun getTestDataPath(): String = "src/test/testData/src/main/" + + @Throws(Exception::class) + override fun setUp() { + super.setUp() + settingJdk() + setDirectoryRoot() + addLibrary("doma-core-3.2.0.jar", "doma-core") + + addEntityJavaFile("User.java") + addEntityJavaFile("UserSummary.java") + addEntityJavaFile("Employee.java") + addEntityJavaFile("EmployeeSummary.java") + addEntityJavaFile("Project.java") + addEntityJavaFile("ProjectDetail.java") + } + + @Throws(Exception::class) + override fun tearDown() { + try { + deleteSdk() + } finally { + super.tearDown() + } + } + + /** + * In GitAction's Test task, if there is no test case in this class, + * a warning will be issued, so this is a workaround. + */ + fun testDao() {} + + @Suppress("SameParameterValue") + private fun addLibrary( + jarName: String, + libName: String, + ) { + val libPath = project.basePath + "/test/lib" + val jarSource = File("src/test/lib/$jarName") + val jarDes = File(libPath, jarName) + FileUtil.copy(jarSource, jarDes) + + PsiTestUtil.addLibrary( + testRootDisposable, + myFixture.module, + libName, + libPath, + jarName, + ) + } + + private fun settingJdk() { + deleteSdk() + setUpJdk(myFixture.module) + } + + private fun setUpJdk(module: Module) { + val newJdk = JavaSdk.getInstance().createJdk("Doma Test JDK", System.getProperty("java.home"), false) + + WriteAction.runAndWait { + ProjectJdkTable.getInstance().addJdk(newJdk) + ModuleRootModificationUtil.updateModel(module) { model: ModifiableRootModel -> + model.sdk = newJdk + } + } + } + + private fun deleteSdk() { + val oldJdk = ProjectJdkTable.getInstance().findJdk("Doma Test JDK") + if (oldJdk == null) return + WriteAction.runAndWait { + ProjectJdkTable.getInstance().removeJdk(oldJdk) + if (oldJdk is Disposable) { + Disposer.dispose(oldJdk) + } + } + } + + private fun setDirectoryRoot() { + val mainDir = myFixture.tempDirFixture.findOrCreateDir("main") + val javaDir = myFixture.tempDirFixture.findOrCreateDir("main/java") + val resourcesDir = myFixture.tempDirFixture.findOrCreateDir(resourceRoot) + WriteAction.runAndWait { + ModuleRootModificationUtil.updateModel(myFixture.module) { model -> + val contentEntry = model.addContentEntry(mainDir) + contentEntry.addSourceFolder(javaDir, JavaSourceRootType.SOURCE) + contentEntry.addSourceFolder(resourcesDir, JavaResourceRootType.RESOURCE) + } + } + } + + fun addDaoJavaFile(vararg fileNames: String) { + for (fileName in fileNames) { + val file = File("$testDataPath/java/$packagePath/dao/$fileName") + myFixture.addFileToProject( + "main/java/$packagePath/dao/$fileName", + file.readText(), + ) + } + } + + private fun addEntityJavaFile(fileName: String) { + val file = File("$testDataPath/java/$packagePath/entity/$fileName") + myFixture.addFileToProject( + "main/java/$packagePath/entity/$fileName", + file.readText(), + ) + } + + fun addResourceFile(vararg sqlFileNames: String) { + for (sqlFileName in sqlFileNames) { + myFixture.addFileToProject( + "$resourceRoot/META-INF/$packagePath/dao/$sqlFileName", + "", + ) + } + } + + fun addSqlFile(vararg sqlNames: String) { + for (sqlName in sqlNames) { + val file = File("$testDataPath/resources/META-INF/$packagePath/dao/$sqlName") + myFixture.addFileToProject( + "$resourceRoot/META-INF/$packagePath/dao/$sqlName", + file.readText(), + ) + } + } + + fun findSqlFile(sqlName: String): VirtualFile? { + val module = myFixture.module + return module?.getResourcesSQLFile( + "$packagePath/dao/$sqlName", + false, + ) + } + + fun findDaoClass(testDaoName: String): PsiClass { + val dao = myFixture.findClass("doma.example.dao.$testDaoName") + assertNotNull("Not Found [$testDaoName]", dao) + return dao + } +} diff --git a/src/test/kotlin/org/domaframework/doma/intellij/complate/sql/SqlCompleteTest.kt b/src/test/kotlin/org/domaframework/doma/intellij/complate/sql/SqlCompleteTest.kt new file mode 100644 index 00000000..df8a9e8d --- /dev/null +++ b/src/test/kotlin/org/domaframework/doma/intellij/complate/sql/SqlCompleteTest.kt @@ -0,0 +1,257 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.complate.sql + +import org.domaframework.doma.intellij.DomaSqlTest +import org.domaframework.doma.intellij.inspection.sql.inspector.SqlBindVariableValidInspector + +/** + * Code completion testing in SQL + */ +class SqlCompleteTest : DomaSqlTest() { + override fun setUp() { + super.setUp() + addDaoJavaFile( + "SqlCompleteTestDao.java", + ) + addSqlFile( + "SqlCompleteTestDao/completeDaoArgument.sql", + "SqlCompleteTestDao/completeInstancePropertyFromDaoArgumentClass.sql", + "SqlCompleteTestDao/completeJavaPackageClass.sql", + "SqlCompleteTestDao/completeDirective.sql", + "SqlCompleteTestDao/completeStaticPropertyFromStaticPropertyCall.sql", + "SqlCompleteTestDao/completePropertyAfterStaticPropertyCall.sql", + "SqlCompleteTestDao/completeBuiltinFunction.sql", + "SqlCompleteTestDao/completeDirectiveInsideIf.sql", + "SqlCompleteTestDao/completeDirectiveInsideElseIf.sql", + "SqlCompleteTestDao/completeDirectiveInsideFor.sql", + "SqlCompleteTestDao/completeDirectiveFieldInsideIf.sql", + "SqlCompleteTestDao/completeDirectiveFieldInsideElseIf.sql", + "SqlCompleteTestDao/completeDirectiveFieldInsideFor.sql", + "SqlCompleteTestDao/completeConcatenationOperator.sql", + "SqlCompleteTestDao/completeComparisonOperator.sql", + ) + myFixture.enableInspections(SqlBindVariableValidInspector()) + } + + fun testCompleteDaoArgument() { + val sqlFile = findSqlFile("SqlCompleteTestDao/completeDaoArgument.sql") + assertNotNull("Not Found SQL File", sqlFile) + if (sqlFile == null) return + + myFixture.configureFromExistingVirtualFile(sqlFile) + val lookupElements = myFixture.completeBasic() + val suggestions = lookupElements.map { it.lookupString } + val expectedSuggestions = listOf("employee", "name") + expectedSuggestions.forEach { expected -> + assertTrue( + "Expected '$expected' in completion suggestions: $suggestions", + suggestions.contains(expected), + ) + } + assertTrue( + "List sizes of [suggestions] and [expectedSuggestions] do not match", + suggestions.size == expectedSuggestions.size, + ) + } + + fun testCompleteInstancePropertyFromDaoArgumentClass() { + innerDirectiveCompleteTest( + "SqlCompleteTestDao/completeInstancePropertyFromDaoArgumentClass.sql", + listOf( + "employeeId", + "employeeName", + "department", + "rank", + "projects", + "getFirstProject()", + ), + listOf("getEmployeeRank()"), + ) + } + + fun testCompleteJavaPackageClass() { + innerDirectiveCompleteTest( + "SqlCompleteTestDao/completeJavaPackageClass.sql", + listOf( + "CASE_INSENSITIVE_ORDER", + "toString()", + "getBytes()", + "toLowerCase()", + "trim()", + "length()", + "isEmpty()", + ), + listOf("value", "hash", "isLatin1()", "isASCII()"), + ) + } + + fun testCompleteDirective() { + innerDirectiveCompleteTest( + "SqlCompleteTestDao/completeDirective.sql", + listOf( + "elseif", + "else", + "end", + "expand", + ), + listOf( + "employee", + "if", + "populate", + "for", + "!", + ), + ) + } + + fun testCompleteStaticPropertyFromStaticPropertyCall() { + innerDirectiveCompleteTest( + "SqlCompleteTestDao/completeStaticPropertyFromStaticPropertyCall.sql", + listOf( + "members", + "projectNumber", + "projectCategory", + "getTermNumber()", + ), + listOf( + "projectDetailId", + "getCategoryName()", + "addTermNumber()", + ), + ) + } + + fun testCompletePropertyAfterStaticPropertyCall() { + innerDirectiveCompleteTest( + "SqlCompleteTestDao/completePropertyAfterStaticPropertyCall.sql", + listOf( + "managerId", + ), + listOf( + "userId", + "employeeName", + ), + ) + } + + fun testCompleteBuiltinFunction() { + innerDirectiveCompleteTest( + "SqlCompleteTestDao/completeBuiltinFunction.sql", + listOf( + "isEmpty()", + "isNotEmpty()", + "isBlank()", + "isNotBlank()", + ), + listOf( + "escape()", + "prefix()", + "infix()", + "suffix()", + "roundDownTimePart()", + "roundUpTimePart()", + ), + ) + } + + fun testCompleteDirectiveInside() { + innerDirectiveCompleteTest( + "SqlCompleteTestDao/completeDirectiveInsideIf.sql", + listOf("employee"), + listOf("project"), + ) + innerDirectiveCompleteTest( + "SqlCompleteTestDao/completeDirectiveInsideElseIf.sql", + listOf("employee", "project"), + emptyList(), + ) + innerDirectiveCompleteTest( + "SqlCompleteTestDao/completeDirectiveInsideFor.sql", + listOf("project"), + listOf("employee"), + ) + } + + fun testCompleteDirectiveFieldInside() { + innerDirectiveCompleteTest( + "SqlCompleteTestDao/completeDirectiveFieldInsideIf.sql", + listOf("startsWith()"), + listOf("employee", "project", "toLowCase"), + ) + innerDirectiveCompleteTest( + "SqlCompleteTestDao/completeDirectiveFieldInsideElseIf.sql", + listOf("department"), + listOf("employee", "project"), + ) + innerDirectiveCompleteTest( + "SqlCompleteTestDao/completeDirectiveFieldInsideFor.sql", + listOf( + "projectId", + "projectName", + "status", + "rank", + "projectNumber", + "projectCategory", + "getFirstEmployee()", + "getTermNumber()", + ), + listOf("project", "getCategoryName()"), + ) + } + + fun testCompleteConcatenationOperator() { + innerDirectiveCompleteTest( + "SqlCompleteTestDao/completeConcatenationOperator.sql", + listOf("rank"), + listOf("employee", "employeeId", "department"), + ) + + innerDirectiveCompleteTest( + "SqlCompleteTestDao/completeComparisonOperator.sql", + listOf("rank"), + listOf("employee", "employeeId", "department"), + ) + } + + private fun innerDirectiveCompleteTest( + sqlFileName: String, + expectedSuggestions: List, + notExpectedSuggestions: List, + ) { + val sqlFile = findSqlFile(sqlFileName) + assertNotNull("Not Found SQL File", sqlFile) + if (sqlFile == null) return + + myFixture.configureFromExistingVirtualFile(sqlFile) + val lookupElements = myFixture.completeBasic() + assertNotNull("No suggestions found", lookupElements) + + val suggestions = lookupElements.map { it.lookupString } + expectedSuggestions.forEach { expected -> + assertTrue( + "Expected '$expected' in completion suggestions: $suggestions", + suggestions.contains(expected), + ) + } + notExpectedSuggestions.forEach { expected -> + assertFalse( + "Expected '$expected' in completion suggestions: $suggestions", + suggestions.contains(expected), + ) + } + } +} diff --git a/src/test/kotlin/org/domaframework/doma/intellij/gutteraction/dao/DaoGenerateSqlActionTest.kt b/src/test/kotlin/org/domaframework/doma/intellij/gutteraction/dao/DaoGenerateSqlActionTest.kt new file mode 100644 index 00000000..2d8ee38d --- /dev/null +++ b/src/test/kotlin/org/domaframework/doma/intellij/gutteraction/dao/DaoGenerateSqlActionTest.kt @@ -0,0 +1,333 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +@file:Suppress("OverrideOnly") + +package org.domaframework.doma.intellij.gutteraction.dao + +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionUiKind +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.fileEditor.FileEditorManager +import org.domaframework.doma.intellij.DomaSqlTest +import java.awt.event.InputEvent +import java.awt.event.MouseEvent + +/** + * Action test to generate SQL file from Dao method + */ +class DaoGenerateSqlActionTest : DomaSqlTest() { + private val packageName = "quickfix" + private val actionId = "org.domaframework.doma.intellij.GenerateSqlAction" + + override fun setUp() { + super.setUp() + addDaoJavaFile( + "quickfix/SelectQuickFixTestDao.java", + "quickfix/InsertQuickFixTestDao.java", + "quickfix/UpdateQuickFixTestDao.java", + "quickfix/DeleteQuickFixTestDao.java", + "quickfix/BatchInsertQuickFixTestDao.java", + "quickfix/BatchUpdateQuickFixTestDao.java", + "quickfix/BatchDeleteQuickFixTestDao.java", + "quickfix/ScriptQuickFixTestDao.java", + "quickfix/SqlProcessorQuickFixTestDao.java", + ) + addDaoJavaFile( + "gutteraction/SelectGutterTestDao.java", + "gutteraction/InsertGutterTestDao.java", + "gutteraction/notdisplayed/UpdateInvalidCaretTestDao.java", + "gutteraction/notdisplayed/DeleteInvalidCaretTestDao.java", + "gutteraction/BatchInsertGutterTestDao.java", + "gutteraction/BatchUpdateGutterTestDao.java", + "gutteraction/notdisplayed/BatchDeleteInvalidCaretTestDao.java", + "gutteraction/ScriptGutterTestDao.java", + "gutteraction/SqlProcessorGutterTestDao.java", + ) + + addResourceFile( + "quickfix/SelectQuickFixTestDao/existsSQLFile.sql", + "quickfix/InsertQuickFixTestDao/existsSQLFile.sql", + "quickfix/UpdateQuickFixTestDao/existsSQLFile.sql", + "quickfix/DeleteQuickFixTestDao/existsSQLFile.sql", + "quickfix/BatchInsertQuickFixTestDao/existsSQLFile.sql", + "quickfix/BatchUpdateQuickFixTestDao/existsSQLFile.sql", + "quickfix/BatchDeleteQuickFixTestDao/existsSQLFile.sql", + "quickfix/ScriptQuickFixTestDao/existsSQLFile.script", + "quickfix/SqlProcessorQuickFixTestDao/existsSQLFile.sql", + ) + addResourceFile( + "gutteraction/SelectGutterTestDao/existsSQLFile1.sql", + "gutteraction/InsertGutterTestDao/existsSQLFile2.sql", + "gutteraction/UpdateGutterTestDao/existsSQLFile1.sql", + "gutteraction/DeleteGutterTestDao/existsSQLFile1.sql", + "gutteraction/BatchInsertGutterTestDao/existsSQLFile2.sql", + "gutteraction/BatchUpdateGutterTestDao/existsSQLFile2.sql", + "gutteraction/BatchDeleteGutterTestDao/existsSQLFile1.sql", + "gutteraction/ScriptGutterTestDao/existsSQLFile2.script", + "gutteraction/SqlProcessorGutterTestDao/existsSQLFile1.sql", + ) + } + + fun testGenerateSQLFileSelect() { + val testDaoName = "SelectQuickFixTestDao" + val daoName = "$packageName.$testDaoName" + val action: AnAction = getActionTest(daoName) + isDisplayedActionTest(action) + generateSqlTest(action, "$testDaoName/generateSQLFile", false) + } + + fun testGenerateSQLFileInsert() { + val testDaoName = "InsertQuickFixTestDao" + val daoName = "$packageName.$testDaoName" + val action: AnAction = getActionTest(daoName) + isDisplayedActionTest(action) + generateSqlTest(action, "$testDaoName/generateSQLFile", false) + } + + fun testGenerateSQLFileUpdate() { + val testDaoName = "UpdateQuickFixTestDao" + val daoName = "$packageName.$testDaoName" + val action: AnAction = getActionTest(daoName) + isDisplayedActionTest(action) + generateSqlTest(action, "$testDaoName/generateSQLFile", false) + } + + fun testGenerateSQLFileDelete() { + val testDaoName = "DeleteQuickFixTestDao" + val daoName = "$packageName.$testDaoName" + val action: AnAction = getActionTest(daoName) + isDisplayedActionTest(action) + generateSqlTest(action, "$testDaoName/generateSQLFile", false) + } + + fun testGenerateSQLFileBatchInsert() { + val testDaoName = "BatchInsertQuickFixTestDao" + val daoName = "$packageName.$testDaoName" + val action: AnAction = getActionTest(daoName) + isDisplayedActionTest(action) + generateSqlTest(action, "$testDaoName/generateSQLFile", false) + } + + fun testGenerateSQLFileBatchUpdate() { + val testDaoName = "BatchUpdateQuickFixTestDao" + val daoName = "$packageName.$testDaoName" + val action: AnAction = getActionTest(daoName) + isDisplayedActionTest(action) + generateSqlTest(action, "$testDaoName/generateSQLFile", false) + } + + fun testGenerateSQLFileBatchDelete() { + val testDaoName = "BatchDeleteQuickFixTestDao" + val daoName = "$packageName.$testDaoName" + val action: AnAction = getActionTest(daoName) + isDisplayedActionTest(action) + generateSqlTest(action, "$testDaoName/generateSQLFile", false) + } + + fun testGenerateSQLFileScript() { + val testDaoName = "ScriptQuickFixTestDao" + val daoName = "$packageName.$testDaoName" + val action: AnAction = getActionTest(daoName) + isDisplayedActionTest(action) + generateSqlTest(action, "$testDaoName/generateSQLFile", true) + } + + fun testGenerateSQLFileSqlProcessor() { + val testDaoName = "SqlProcessorQuickFixTestDao" + val daoName = "$packageName.$testDaoName" + val action: AnAction = getActionTest(daoName) + isDisplayedActionTest(action) + generateSqlTest(action, "$testDaoName/generateSQLFile", false) + } + + fun testNonGenerateSQLFileSelect() { + val testDaoName = "SelectGutterTestDao" + val daoName = "gutteraction.$testDaoName" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canNotGenerateSqlFileTest(action, "$testDaoName/existsSQLFile1.sql", false) + } + + fun testNonGenerateSQLFileInsert() { + val testDaoName = "InsertGutterTestDao" + val daoName = "gutteraction.$testDaoName" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canNotGenerateSqlFileTest(action, "$testDaoName/existsSQLFile2", false) + } + + fun testNonGenerateSQLFileUpdate() { + val testDaoName = "UpdateInvalidCaretTestDao" + val daoName = "gutteraction.$testDaoName" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canNotGenerateSqlFileTest(action, "$testDaoName/nonRequireSQLFile.sql", false) + } + + fun testNonGenerateSQLFileDelete() { + val testDaoName = "DeleteInvalidCaretTestDao" + val daoName = "gutteraction.$testDaoName" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canNotGenerateSqlFileTest(action, "$testDaoName/NoSQLFileRequired", false) + } + + fun testNonGenerateSQLFileBatchInsert() { + val testDaoName = "BatchInsertGutterTestDao" + val daoName = "gutteraction.$testDaoName" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canNotGenerateSqlFileTest(action, "$testDaoName/existsSQLFile2", false) + } + + fun testNonGenerateSQLFileBatchUpdate() { + val testDaoName = "BatchUpdateGutterTestDao" + val daoName = "gutteraction.$testDaoName" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canNotGenerateSqlFileTest(action, "$testDaoName/existsSQLFile2", false) + } + + fun testNonGenerateSQLFileBatchDelete() { + val testDaoName = "BatchDeleteInvalidCaretTestDao" + val daoName = "gutteraction.$testDaoName" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canNotGenerateSqlFileTest(action, "$testDaoName/NoSQLFileRequired", false) + } + + fun testNonGenerateSQLFileScript() { + val testDaoName = "ScriptGutterTestDao" + val daoName = "gutteraction.$testDaoName" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canNotGenerateSqlFileTest(action, "$testDaoName/existsSQLFile2", true) + } + + fun testNonGenerateSQLFileSqlProcessor() { + val testDaoName = "SqlProcessorGutterTestDao" + val daoName = "gutteraction.$testDaoName" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canNotGenerateSqlFileTest(action, "$testDaoName/existsSQLFile2", false) + } + + private fun getActionTest(daoName: String): AnAction { + val dao = findDaoClass(daoName) + myFixture.configureFromExistingVirtualFile(dao.containingFile.virtualFile) + + val action: AnAction = ActionManager.getInstance().getAction(actionId) + assertNotNull("Not Found Action", action) + return action + } + + private fun isDisplayedActionTest(action: AnAction) { + val event = createEventAction() + + action.update(event) + val presentation = event.presentation + assertTrue("Action should be visible in popup", presentation.isVisible) + assertTrue("Action should be enabled in popup", presentation.isEnabled) + } + + private fun isNotDisplayedActionTest(action: AnAction) { + val event = createEventAction() + + action.update(event) + val presentation = event.presentation + assertFalse("Action should be not visible in popup", presentation.isVisible) + assertFalse("Action should be disabled in popup", presentation.isEnabled) + } + + private fun createEventAction(): AnActionEvent { + val editor = myFixture.editor + val file = myFixture.file + val dataContext = + DataContext { dataId: String? -> + if (CommonDataKeys.EDITOR.`is`(dataId)) { + return@DataContext editor + } + if (CommonDataKeys.PSI_FILE.`is`(dataId)) { + return@DataContext file + } + if (CommonDataKeys.PROJECT.`is`(dataId)) { + return@DataContext myFixture.project + } + null + } + + val mouseEvent = + MouseEvent( + editor.component, + MouseEvent.MOUSE_CLICKED, + System.currentTimeMillis(), + InputEvent.BUTTON3_DOWN_MASK, + 0, + 0, + 1, + false, + ) + + val event = + AnActionEvent.createEvent( + dataContext, + null, + "EditorPopup", + ActionUiKind.POPUP, + mouseEvent, + ) + return event + } + + private fun generateSqlTest( + action: AnAction, + sqlFileName: String, + isScript: Boolean, + ) { + myFixture.testAction(action) + + val sqlFile = "$sqlFileName.${if (isScript) "script" else "sql"}" + val openedEditor = FileEditorManager.getInstance(project).selectedEditors + assertTrue( + "Open File is Not $sqlFileName", + openedEditor.any { it.file.name == sqlFile.substringAfter("/") }, + ) + + val generatedSql = findSqlFile("$packageName/$sqlFile") + assertTrue("Not Found SQL File [$sqlFile]", generatedSql != null) + } + + private fun canNotGenerateSqlFileTest( + action: AnAction, + sqlFileName: String, + isScript: Boolean, + ) { + myFixture.testAction(action) + + val sqlFile = "$sqlFileName.${if (isScript) "script" else "sql"}" + val openedEditor = FileEditorManager.getInstance(project).selectedEditors + assertFalse( + "Open File is [$sqlFile]", + openedEditor.any { it.file.name == sqlFile.substringAfter("/") }, + ) + + val generatedSql = findSqlFile("$packageName/$sqlFile") + assertFalse("Not subject to action", generatedSql != null) + } +} diff --git a/src/test/kotlin/org/domaframework/doma/intellij/gutteraction/dao/DaoGutterActionTest.kt b/src/test/kotlin/org/domaframework/doma/intellij/gutteraction/dao/DaoGutterActionTest.kt new file mode 100644 index 00000000..a56a9b6f --- /dev/null +++ b/src/test/kotlin/org/domaframework/doma/intellij/gutteraction/dao/DaoGutterActionTest.kt @@ -0,0 +1,199 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.gutteraction.dao + +import com.intellij.codeInsight.daemon.GutterIconNavigationHandler +import com.intellij.codeInsight.daemon.GutterMark +import com.intellij.codeInsight.daemon.LineMarkerInfo +import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.psi.PsiElement +import org.domaframework.doma.intellij.DomaSqlTest +import org.domaframework.doma.intellij.bundle.MessageBundle +import org.domaframework.doma.intellij.setting.SqlIcon + +/** + * Dao gutter icon display, action execution test + */ +class DaoGutterActionTest : DomaSqlTest() { + private val packageName = "gutteraction" + + override fun setUp() { + super.setUp() + + addDaoJavaFile( + "$packageName/SelectGutterTestDao.java", + "$packageName/InsertGutterTestDao.java", + "$packageName/UpdateGutterTestDao.java", + "$packageName/DeleteGutterTestDao.java", + "$packageName/BatchInsertGutterTestDao.java", + "$packageName/BatchUpdateGutterTestDao.java", + "$packageName/BatchDeleteGutterTestDao.java", + "$packageName/ScriptGutterTestDao.java", + "$packageName/SqlProcessorGutterTestDao.java", + ) + addResourceFile( + "$packageName/SelectGutterTestDao/existsSQLFile1.sql", + "$packageName/InsertGutterTestDao/existsSQLFile1.sql", + "$packageName/UpdateGutterTestDao/existsSQLFile1.sql", + "$packageName/DeleteGutterTestDao/existsSQLFile1.sql", + "$packageName/BatchInsertGutterTestDao/existsSQLFile1.sql", + "$packageName/BatchUpdateGutterTestDao/existsSQLFile1.sql", + "$packageName/BatchDeleteGutterTestDao/existsSQLFile1.sql", + "$packageName/ScriptGutterTestDao/existsSQLFile1.script", + "$packageName/SqlProcessorGutterTestDao/existsSQLFile1.sql", + ) + addResourceFile( + "$packageName/SelectGutterTestDao/existsSQLFile2.sql", + "$packageName/InsertGutterTestDao/existsSQLFile2.sql", + "$packageName/UpdateGutterTestDao/existsSQLFile2.sql", + "$packageName/DeleteGutterTestDao/existsSQLFile2.sql", + "$packageName/BatchInsertGutterTestDao/existsSQLFile2.sql", + "$packageName/BatchUpdateGutterTestDao/existsSQLFile2.sql", + "$packageName/BatchDeleteGutterTestDao/existsSQLFile2.sql", + "$packageName/ScriptGutterTestDao/existsSQLFile2.script", + "$packageName/SqlProcessorGutterTestDao/existsSQLFile2.sql", + ) + addResourceFile( + "$packageName/BatchInsertGutterTestDao/existsSQLFile3.sql", + "$packageName/BatchUpdateGutterTestDao/existsSQLFile3.sql", + "$packageName/BatchDeleteGutterTestDao/existsSQLFile3.sql", + ) + } + + fun testSelectDisplayGutter() { + val daoName = "$packageName.SelectGutterTestDao" + val total = 2 + val targetGutter = gutterIconsDisplayedTest(daoName, total) + gutterIconNavigation("existsSQLFile1.sql", targetGutter) + } + + fun testInsertDisplayGutter() { + val daoName = "$packageName.InsertGutterTestDao" + val total = 2 + val targetGutter = gutterIconsDisplayedTest(daoName, total) + gutterIconNavigation("existsSQLFile1.sql", targetGutter) + } + + fun testUpdateDisplayGutter() { + val daoName = "$packageName.UpdateGutterTestDao" + val total = 2 + val targetGutter = gutterIconsDisplayedTest(daoName, total) + gutterIconNavigation("existsSQLFile1.sql", targetGutter) + } + + fun testDeleteDisplayGutter() { + val daoName = "$packageName.DeleteGutterTestDao" + val total = 1 + val targetGutter = gutterIconsDisplayedTest(daoName, total) + gutterIconNavigation("existsSQLFile1.sql", targetGutter) + } + + fun testBatchInsertDisplayGutter() { + val daoName = "$packageName.BatchInsertGutterTestDao" + val total = 3 + val targetGutter = gutterIconsDisplayedTest(daoName, total) + gutterIconNavigation("existsSQLFile1.sql", targetGutter) + } + + fun testBatchUpdateDisplayGutter() { + val daoName = "$packageName.BatchUpdateGutterTestDao" + val total = 3 + val targetGutter = gutterIconsDisplayedTest(daoName, total) + gutterIconNavigation("existsSQLFile1.sql", targetGutter) + } + + fun testBatchDeleteDisplayGutter() { + val daoName = "$packageName.BatchDeleteGutterTestDao" + val total = 2 + val targetGutter = gutterIconsDisplayedTest(daoName, total) + gutterIconNavigation("existsSQLFile1.sql", targetGutter) + } + + fun testScriptDisplayGutter() { + val daoName = "$packageName.ScriptGutterTestDao" + val total = 2 + val targetGutter = gutterIconsDisplayedTest(daoName, total) + gutterIconNavigation("existsSQLFile1.script", targetGutter) + } + + fun testSqlProcessorDisplayGutter() { + val daoName = "$packageName.SqlProcessorGutterTestDao" + val total = 2 + val targetGutter = gutterIconsDisplayedTest(daoName, total) + gutterIconNavigation("existsSQLFile1.sql", targetGutter) + } + + private fun gutterIconsDisplayedTest( + daoName: String, + total: Int, + ): LineMarkerInfo<*>? { + val dao = findDaoClass(daoName) + val targetElementNames = listOf("existsSQLFile1", "existsSQLFile2", "existsSQLFile3") + + myFixture.configureFromExistingVirtualFile(dao.containingFile.virtualFile) + myFixture.doHighlighting() + + val gutters: List = myFixture.findAllGutters() + var found = 0 + var firstGutter: LineMarkerInfo? = null + + for (mark in gutters) { + if (MessageBundle.message("jump.to.sql.tooltip.title") == mark.tooltipText && + mark is LineMarkerInfo.LineMarkerGutterIconRenderer<*> + ) { + val markTargetName = mark.lineMarkerInfo.element?.text + assertTrue( + "Unexpected gutter icon found $markTargetName", + targetElementNames.contains(markTargetName), + ) + + found++ + assertEquals( + SqlIcon.FILE, + mark.icon, + ) + if (firstGutter == null) firstGutter = mark.lineMarkerInfo + } + } + assertTrue("Expected gutter icon was not found found:$found total:$total", found == total) + + return firstGutter + } + + private fun gutterIconNavigation( + sqlName: String, + gutterIcon: LineMarkerInfo<*>?, + ) { + assertNotNull("Gutter is null", gutterIcon) + val targetMarkerInfo: RelatedItemLineMarkerInfo<*> = gutterIcon as RelatedItemLineMarkerInfo<*> + + assertNotNull("Expected RelatedItemLineMarkerInfo was not found", targetMarkerInfo) + + val navHandler = targetMarkerInfo.navigationHandler + assertNotNull("Navigation handler is null", navHandler) + + val markerElement = targetMarkerInfo.element + assertNotNull("Marker element is null", markerElement) + + val typedNavHandler = + navHandler as GutterIconNavigationHandler<*> + typedNavHandler.navigate(null, null) + + val editor = FileEditorManager.getInstance(project).selectedEditors + assertTrue("Ope File is Not $sqlName", editor.any { it.file.name == sqlName }) + } +} diff --git a/src/test/kotlin/org/domaframework/doma/intellij/gutteraction/dao/DaoJumpActionTest.kt b/src/test/kotlin/org/domaframework/doma/intellij/gutteraction/dao/DaoJumpActionTest.kt new file mode 100644 index 00000000..0bfd93f1 --- /dev/null +++ b/src/test/kotlin/org/domaframework/doma/intellij/gutteraction/dao/DaoJumpActionTest.kt @@ -0,0 +1,314 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +@file:Suppress("OverrideOnly") + +package org.domaframework.doma.intellij.gutteraction.dao + +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionUiKind +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.fileEditor.FileEditorManager +import org.domaframework.doma.intellij.DomaSqlTest +import java.awt.event.InputEvent +import java.awt.event.MouseEvent + +/** + * Action test for jumping from Dao method to SQL file + */ +class DaoJumpActionTest : DomaSqlTest() { + private val packageName = "gutteraction" + private val actionId = "org.domaframework.doma.intellij.action.JumpToSQLFromDao" + + override fun setUp() { + super.setUp() + addDaoJavaFile( + "$packageName/SelectGutterTestDao.java", + "$packageName/InsertGutterTestDao.java", + "$packageName/UpdateGutterTestDao.java", + "$packageName/DeleteGutterTestDao.java", + "$packageName/BatchInsertGutterTestDao.java", + "$packageName/BatchUpdateGutterTestDao.java", + "$packageName/BatchDeleteGutterTestDao.java", + "$packageName/ScriptGutterTestDao.java", + "$packageName/SqlProcessorGutterTestDao.java", + ) + val notDisplayedPackage = "notdisplayed" + addDaoJavaFile( + "$packageName/$notDisplayedPackage/SelectInvalidCaretTestDao.java", + "$packageName/$notDisplayedPackage/InsertInvalidCaretTestDao.java", + "$packageName/$notDisplayedPackage/UpdateInvalidCaretTestDao.java", + "$packageName/$notDisplayedPackage/DeleteInvalidCaretTestDao.java", + "$packageName/$notDisplayedPackage/BatchInsertInvalidCaretTestDao.java", + "$packageName/$notDisplayedPackage/BatchUpdateInvalidCaretTestDao.java", + "$packageName/$notDisplayedPackage/BatchDeleteInvalidCaretTestDao.java", + "$packageName/$notDisplayedPackage/ScriptInvalidCaretTestDao.java", + "$packageName/$notDisplayedPackage/SqlProcessorInvalidCaretTestDao.java", + "$packageName/$notDisplayedPackage/InvalidCaretTestDao.java", + ) + addResourceFile( + "$packageName/SelectGutterTestDao/existsSQLFile1.sql", + "$packageName/InsertGutterTestDao/existsSQLFile1.sql", + "$packageName/UpdateGutterTestDao/existsSQLFile1.sql", + "$packageName/DeleteGutterTestDao/existsSQLFile1.sql", + "$packageName/BatchInsertGutterTestDao/existsSQLFile1.sql", + "$packageName/BatchUpdateGutterTestDao/existsSQLFile1.sql", + "$packageName/BatchDeleteGutterTestDao/existsSQLFile1.sql", + "$packageName/ScriptGutterTestDao/existsSQLFile1.script", + "$packageName/SqlProcessorGutterTestDao/existsSQLFile1.sql", + ) + addResourceFile( + "$packageName/SelectGutterTestDao/existsSQLFile2.sql", + "$packageName/InsertGutterTestDao/existsSQLFile2.sql", + "$packageName/UpdateGutterTestDao/existsSQLFile2.sql", + "$packageName/DeleteGutterTestDao/existsSQLFile2.sql", + "$packageName/BatchInsertGutterTestDao/existsSQLFile2.sql", + "$packageName/BatchUpdateGutterTestDao/existsSQLFile2.sql", + "$packageName/BatchDeleteGutterTestDao/existsSQLFile2.sql", + "$packageName/ScriptGutterTestDao/existsSQLFile2.script", + "$packageName/SqlProcessorGutterTestDao/existsSQLFile2.sql", + ) + } + + fun testSelectJumpToSqlAction() { + val daoName = "$packageName.SelectGutterTestDao" + val sqlFileName = "existsSQLFile1.sql" + val action: AnAction = getActionTest(daoName) + isDisplayedActionTest(action) + jumpToSqlTest(action, sqlFileName) + } + + fun testInsertJumpToSqlAction() { + val daoName = "$packageName.InsertGutterTestDao" + val sqlFileName = "existsSQLFile2.sql" + val action: AnAction = getActionTest(daoName) + isDisplayedActionTest(action) + jumpToSqlTest(action, sqlFileName) + } + + fun testUpdateJumpToSqlAction() { + val daoName = "$packageName.UpdateGutterTestDao" + val sqlFileName = "existsSQLFile1.sql" + val action: AnAction = getActionTest(daoName) + isDisplayedActionTest(action) + jumpToSqlTest(action, sqlFileName) + } + + fun testDeleteJumpToSqlAction() { + val daoName = "$packageName.DeleteGutterTestDao" + val sqlFileName = "existsSQLFile1.sql" + val action: AnAction = getActionTest(daoName) + isDisplayedActionTest(action) + jumpToSqlTest(action, sqlFileName) + } + + fun testBatchInsertJumpToSqlAction() { + val daoName = "$packageName.BatchInsertGutterTestDao" + val sqlFileName = "existsSQLFile2.sql" + val action: AnAction = getActionTest(daoName) + isDisplayedActionTest(action) + jumpToSqlTest(action, sqlFileName) + } + + fun testBatchUpdateJumpToSqlAction() { + val daoName = "$packageName.BatchUpdateGutterTestDao" + val sqlFileName = "existsSQLFile2.sql" + val action: AnAction = getActionTest(daoName) + isDisplayedActionTest(action) + jumpToSqlTest(action, sqlFileName) + } + + fun testBatchDeleteJumpToSqlAction() { + val daoName = "$packageName.BatchDeleteGutterTestDao" + val sqlFileName = "existsSQLFile2.sql" + val action: AnAction = getActionTest(daoName) + isDisplayedActionTest(action) + jumpToSqlTest(action, sqlFileName) + } + + fun testScriptJumpToSqlAction() { + val daoName = "$packageName.ScriptGutterTestDao" + val sqlFileName = "existsSQLFile2.script" + val action: AnAction = getActionTest(daoName) + isDisplayedActionTest(action) + jumpToSqlTest(action, sqlFileName) + } + + fun testSqlProcessorJumpToSqlAction() { + val daoName = "$packageName.SqlProcessorGutterTestDao" + val sqlFileName = "existsSQLFile1.sql" + val action: AnAction = getActionTest(daoName) + isDisplayedActionTest(action) + jumpToSqlTest(action, sqlFileName) + } + + fun testSelectNotDisplayJumpToSql() { + val daoName = "$packageName.SelectInvalidCaretTestDao" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canSqlTest(action, "nonExistSQLFile.sql") + } + + fun testInsertNotDisplayJumpToSql() { + val daoName = "$packageName.InsertInvalidCaretTestDao" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canSqlTest(action, "nonExistSQLFileAndTemplateIncludedList.sql") + } + + fun testUpdateNotDisplayJumpToSql() { + val daoName = "$packageName.UpdateInvalidCaretTestDao" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canSqlTest(action, "nonRequireSQLFile.sql") + } + + fun testDeleteNotDisplayJumpToSql() { + val daoName = "$packageName.DeleteInvalidCaretTestDao" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canSqlTest(action, "nonRequireSQLFile.sql") + } + + fun testBatchInsertNotDisplayJumpToSql() { + val daoName = "$packageName.BatchInsertInvalidCaretTestDao" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canSqlTest(action, "nonExistSQLFileError.sql") + } + + fun testBatchUpdateNotDisplayJumpToSql() { + val daoName = "$packageName.BatchUpdateInvalidCaretTestDao" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canSqlTest(action, "nonExistSQLFileAndTemplateIncludedList.sql") + } + + fun testBatchDeleteNotDisplayJumpToSql() { + val daoName = "$packageName.BatchDeleteInvalidCaretTestDao" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canSqlTest(action, "nonRequireSQLFile.sql") + } + + fun testScriptNotDisplayJumpToSql() { + val daoName = "$packageName.ScriptInvalidCaretTestDao" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canSqlTest(action, "nonExistSQLFileAndTemplateIncludedList.script") + } + + fun testSqlProcessorNotDisplayJumpToSql() { + val daoName = "$packageName.SqlProcessorInvalidCaretTestDao" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canSqlTest(action, "nonExistSQLFile.sql") + } + + fun testSqlNotDisplayJumpToSql() { + val daoName = "$packageName.InvalidCaretTestDao" + val action: AnAction = getActionTest(daoName) + isNotDisplayedActionTest(action) + canSqlTest(action, "nonExistSQLFile.sql") + } + + private fun getActionTest(daoName: String): AnAction { + val dao = findDaoClass(daoName) + myFixture.configureFromExistingVirtualFile(dao.containingFile.virtualFile) + + val action: AnAction = ActionManager.getInstance().getAction(actionId) + assertNotNull("Not Found Action", action) + return action + } + + private fun isDisplayedActionTest(action: AnAction) { + val event = createEventAction() + + action.update(event) + val presentation = event.presentation + assertTrue("Action should be visible in popup", presentation.isVisible) + assertTrue("Action should be enabled in popup", presentation.isEnabled) + } + + private fun isNotDisplayedActionTest(action: AnAction) { + val event = createEventAction() + + action.update(event) + val presentation = event.presentation + assertFalse("Action should be not visible in popup", presentation.isVisible) + assertFalse("Action should be disabled in popup", presentation.isEnabled) + } + + private fun createEventAction(): AnActionEvent { + val editor = myFixture.editor + val file = myFixture.file + val dataContext = + DataContext { dataId: String? -> + if (CommonDataKeys.EDITOR.`is`(dataId)) { + return@DataContext editor + } + if (CommonDataKeys.PSI_FILE.`is`(dataId)) { + return@DataContext file + } + if (CommonDataKeys.PROJECT.`is`(dataId)) { + return@DataContext myFixture.project + } + null + } + + val mouseEvent = + MouseEvent( + editor.component, + MouseEvent.MOUSE_CLICKED, + System.currentTimeMillis(), + InputEvent.BUTTON3_DOWN_MASK, + 0, + 0, + 1, + false, + ) + + val event = + AnActionEvent.createEvent( + dataContext, + null, + "EditorPopup", + ActionUiKind.POPUP, + mouseEvent, + ) + return event + } + + private fun jumpToSqlTest( + action: AnAction, + sqlFileName: String, + ) { + myFixture.testAction(action) + val openedEditor = FileEditorManager.getInstance(project).selectedEditors + assertTrue("Ope File is Not $sqlFileName", openedEditor.any { it.file.name == sqlFileName }) + } + + private fun canSqlTest( + action: AnAction, + sqlFileName: String, + ) { + myFixture.testAction(action) + val openedEditor = FileEditorManager.getInstance(project).selectedEditors + assertFalse("Ope File is $sqlFileName", openedEditor.any { it.file.name == sqlFileName }) + } +} diff --git a/src/test/kotlin/org/domaframework/doma/intellij/gutteraction/sql/SqlGutterActionTest.kt b/src/test/kotlin/org/domaframework/doma/intellij/gutteraction/sql/SqlGutterActionTest.kt new file mode 100644 index 00000000..c7034fa7 --- /dev/null +++ b/src/test/kotlin/org/domaframework/doma/intellij/gutteraction/sql/SqlGutterActionTest.kt @@ -0,0 +1,107 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.gutteraction.sql + +import com.intellij.codeInsight.daemon.GutterIconNavigationHandler +import com.intellij.codeInsight.daemon.GutterMark +import com.intellij.codeInsight.daemon.LineMarkerInfo +import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.psi.PsiElement +import org.domaframework.doma.intellij.DomaSqlTest +import org.domaframework.doma.intellij.bundle.MessageBundle + +/** + * SQL gutter icon display, action execution test + */ +class SqlGutterActionTest : DomaSqlTest() { + private val packageName = "gutteraction" + + override fun setUp() { + super.setUp() + + addDaoJavaFile("$packageName/JumpActionTestDao.java") + addSqlFile( + "$packageName/JumpActionTestDao/jumpToDaoFile.sql", + "$packageName/JumpActionTestDao/notDisplayGutterWithNonExistentDaoMethod.sql", + ) + } + + fun testSqlDisplayGutter() { + val sqlName = "$packageName/JumpActionTestDao/jumpToDaoFile.sql" + val total = 1 + val targetGutter = gutterIconsDisplayedTest(sqlName, total) + gutterIconNavigation("JumpActionTestDao.java", targetGutter) + } + + fun testSqlNonDisplayGutter() { + val sqlName = "$packageName/JumpActionTestDao/notDisplayGutterWithNonExistentDaoMethod.sql" + val total = 0 + val targetGutter = gutterIconsDisplayedTest(sqlName, total) + assertNull("Gutter is displayed", targetGutter) + } + + private fun gutterIconsDisplayedTest( + sqlName: String, + total: Int, + ): LineMarkerInfo<*>? { + val sql = findSqlFile(sqlName) + assertNotNull("Not Found SQL File", sql) + if (sql == null) return null + + myFixture.configureFromExistingVirtualFile(sql) + myFixture.doHighlighting() + + val gutters: List = myFixture.findAllGutters() + var found = 0 + var firstGutter: LineMarkerInfo? = null + + for (mark in gutters) { + if (MessageBundle.message("jump.to.dao.tooltip.title") == mark.tooltipText && + mark is LineMarkerInfo.LineMarkerGutterIconRenderer<*> + ) { + found++ + if (firstGutter == null) firstGutter = mark.lineMarkerInfo + } + } + assertTrue("Expected gutter icon was not found found:$found total:$total", found == total) + + return firstGutter + } + + private fun gutterIconNavigation( + daoName: String, + gutterIcon: LineMarkerInfo<*>?, + ) { + assertNotNull("Gutter is null", gutterIcon) + val targetMarkerInfo: RelatedItemLineMarkerInfo<*> = gutterIcon as RelatedItemLineMarkerInfo<*> + + assertNotNull("Expected RelatedItemLineMarkerInfo was not found", targetMarkerInfo) + + val navHandler = targetMarkerInfo.navigationHandler + assertNotNull("Navigation handler is null", navHandler) + + val markerElement = targetMarkerInfo.element + assertNotNull("Marker element is null", markerElement) + + val typedNavHandler = + navHandler as GutterIconNavigationHandler<*> + typedNavHandler.navigate(null, null) + + val editor = FileEditorManager.getInstance(project).selectedEditors + assertTrue("Ope File is Not $daoName", editor.any { it.file.name == daoName }) + } +} diff --git a/src/test/kotlin/org/domaframework/doma/intellij/gutteraction/sql/SqlJumpActionTest.kt b/src/test/kotlin/org/domaframework/doma/intellij/gutteraction/sql/SqlJumpActionTest.kt new file mode 100644 index 00000000..cdb0ac6f --- /dev/null +++ b/src/test/kotlin/org/domaframework/doma/intellij/gutteraction/sql/SqlJumpActionTest.kt @@ -0,0 +1,250 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +@file:Suppress("OverrideOnly") + +package org.domaframework.doma.intellij.gutteraction.sql + +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionUiKind +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.fileEditor.FileEditorManager +import org.domaframework.doma.intellij.DomaSqlTest +import java.awt.event.InputEvent +import java.awt.event.MouseEvent + +/** + * Action test for jumping from SQL file to Dao method + */ +class SqlJumpActionTest : DomaSqlTest() { + private val packageName = "gutteraction" + + override fun setUp() { + super.setUp() + addDaoJavaFile("$packageName/JumpActionTestDao.java") + addSqlFile( + "$packageName/JumpActionTestDao/jumpToDaoFile.sql", + "$packageName/JumpActionTestDao/notDisplayGutterWithNonExistentDaoMethod.sql", + "$packageName/JumpActionTestDao/jumpToDaoMethodArgumentDefinition.sql", + "$packageName/JumpActionTestDao/jumpToClassFieldDefinition.sql", + "$packageName/JumpActionTestDao/jumpsToClassMethodDefinition.sql", + "$packageName/JumpActionTestDao/jumpToStaticFieldDefinition.sql", + "$packageName/JumpActionTestDao/jumpToStaticMethodDefinition.sql", + ) + } + + fun testJumpActionFromSQLFileToDaoMethodCanBePerformed() { + val sqlName = "$packageName/JumpActionTestDao/jumpToDaoFile.sql" + val action: AnAction? = getJumDaoActionTest(sqlName) + assertNotNull("Not Found Action", action) + if (action == null) return + + isDisplayedActionTest(action) + jumpToDaoTest(action, "JumpActionTestDao.java") + } + + fun testNotDisplayedJumpMethodActionInSQLForNonExistentDaoMethod() { + val sqlName = "$packageName/JumpActionTestDao/notDisplayGutterWithNonExistentDaoMethod.sql" + val action: AnAction? = getJumDaoActionTest(sqlName) + assertNotNull("Not Found Action", action) + if (action == null) return + + isNotDisplayedActionTest(action) + canNotJumpToDaoTest(action, "JumpActionTestDao.java") + } + + fun testNotDisplayedJumpDeclarationActionInSQLForNonExistentDaoMethod() { + val sqlName = "$packageName/JumpActionTestDao/notDisplayGutterWithNonExistentDaoMethod.sql" + val action: AnAction? = getJumDeclarationActionTest(sqlName) + assertNotNull("Not Found Action", action) + if (action == null) return + + isNotDisplayedActionTest(action) + canNotJumpToDaoTest(action, "JumpActionTestDao.java") + } + + fun testJumpToDaoMethodArgumentDefinition() { + val sqlName = "$packageName/JumpActionTestDao/jumpToDaoMethodArgumentDefinition.sql" + val action: AnAction? = getJumpVariableActionTest(sqlName) + assertNotNull("Not Found Action", action) + if (action == null) return + + isDisplayedActionTest(action) + jumpToDaoTest(action, "JumpActionTestDao.java") + } + + fun testJumpToClassFieldDefinition() { + val sqlName = "$packageName/JumpActionTestDao/jumpToClassFieldDefinition.sql" + val action: AnAction? = getJumpVariableActionTest(sqlName) + assertNotNull("Not Found Action", action) + if (action == null) return + + isDisplayedActionTest(action) + jumpToDaoTest(action, "Project.java") + } + + fun testJumpsToClassMethodDefinition() { + val sqlName = "$packageName/JumpActionTestDao/jumpsToClassMethodDefinition.sql" + val action: AnAction? = getJumpVariableActionTest(sqlName) + assertNotNull("Not Found Action", action) + if (action == null) return + + isDisplayedActionTest(action) + jumpToDaoTest(action, "Employee.java") + } + + fun testJumpToStaticFieldDefinition() { + val sqlName = "$packageName/JumpActionTestDao/jumpToStaticFieldDefinition.sql" + val action: AnAction? = getJumpVariableActionTest(sqlName) + assertNotNull("Not Found Action", action) + if (action == null) return + + isDisplayedActionTest(action) + jumpToDaoTest(action, "ProjectDetail.java") + } + + fun testJumpToStaticMethodDefinition() { + val sqlName = "$packageName/JumpActionTestDao/jumpToStaticMethodDefinition.sql" + val action: AnAction? = getJumpVariableActionTest(sqlName) + assertNotNull("Not Found Action", action) + if (action == null) return + + isDisplayedActionTest(action) + jumpToDaoTest(action, "ProjectDetail.java") + } + + private fun getJumDaoActionTest(sqlName: String): AnAction? { + val sql = findSqlFile(sqlName) + assertNotNull("Not Found SQL File", sql) + if (sql == null) return null + + myFixture.configureFromExistingVirtualFile(sql) + + val actionId = "org.domaframework.doma.intellij.JumpToDaoFromSQL" + val action: AnAction = ActionManager.getInstance().getAction(actionId) + assertNotNull("Not Found Action", action) + return action + } + + private fun getJumDeclarationActionTest(sqlName: String): AnAction? { + val sql = findSqlFile(sqlName) + assertNotNull("Not Found SQL File", sql) + if (sql == null) return null + + myFixture.configureFromExistingVirtualFile(sql) + + val actionId = "org.domaframework.doma.intellij.JumpToDeclarationFromSql" + val action: AnAction = ActionManager.getInstance().getAction(actionId) + assertNotNull("Not Found Action", action) + return action + } + + private fun getJumpVariableActionTest(sqlName: String): AnAction? { + val sql = findSqlFile(sqlName) + assertNotNull("Not Found SQL File", sql) + if (sql == null) return null + + myFixture.configureFromExistingVirtualFile(sql) + + val actionId = "org.domaframework.doma.intellij.JumpToDeclarationFromSql" + val action: AnAction = ActionManager.getInstance().getAction(actionId) + assertNotNull("Not Found Action", action) + return action + } + + private fun isDisplayedActionTest(action: AnAction) { + val event = createEventAction() + + action.update(event) + val presentation = event.presentation + assertTrue("Action should be visible in popup", presentation.isVisible) + assertTrue("Action should be enabled in popup", presentation.isEnabled) + } + + private fun isNotDisplayedActionTest(action: AnAction) { + val event = createEventAction() + + action.update(event) + val presentation = event.presentation + assertFalse("Action should be not visible in popup", presentation.isVisible) + assertFalse("Action should be disabled in popup", presentation.isEnabled) + } + + private fun createEventAction(): AnActionEvent { + val editor = myFixture.editor + val file = myFixture.file + val dataContext = + DataContext { dataId: String? -> + if (CommonDataKeys.EDITOR.`is`(dataId)) { + return@DataContext editor + } + if (CommonDataKeys.PSI_FILE.`is`(dataId)) { + return@DataContext file + } + if (CommonDataKeys.PROJECT.`is`(dataId)) { + return@DataContext myFixture.project + } + null + } + + val mouseEvent = + MouseEvent( + editor.component, + MouseEvent.MOUSE_CLICKED, + System.currentTimeMillis(), + InputEvent.BUTTON3_DOWN_MASK, + 0, + 0, + 1, + false, + ) + + val event = + AnActionEvent.createEvent( + dataContext, + null, + "EditorPopup", + ActionUiKind.POPUP, + mouseEvent, + ) + return event + } + + private fun jumpToDaoTest( + action: AnAction, + openFileName: String, + ) { + myFixture.testAction(action) + + val openedEditor = FileEditorManager.getInstance(project).selectedEditors + assertTrue( + "Open File is Not [${openedEditor.map { it.file.name }}] $openFileName", + openedEditor.any { it.file.name == openFileName }, + ) + } + + @Suppress("SameParameterValue") + private fun canNotJumpToDaoTest( + action: AnAction, + daoFileName: String, + ) { + myFixture.testAction(action) + val openedEditor = FileEditorManager.getInstance(project).selectedEditors + assertFalse("Ope File is $daoFileName", openedEditor.any { it.file.name == daoFileName }) + } +} diff --git a/src/test/kotlin/org/domaframework/doma/intellij/inspection/dao/DomaSqlExistTest.kt b/src/test/kotlin/org/domaframework/doma/intellij/inspection/dao/DomaSqlExistTest.kt new file mode 100644 index 00000000..ae3d6b73 --- /dev/null +++ b/src/test/kotlin/org/domaframework/doma/intellij/inspection/dao/DomaSqlExistTest.kt @@ -0,0 +1,138 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.inspection.dao + +import org.domaframework.doma.intellij.DomaSqlTest +import org.domaframework.doma.intellij.inspection.dao.inspector.SqlFileExistInspector + +class DomaSqlExistTest : DomaSqlTest() { + override fun setUp() { + super.setUp() + addDaoJavaFile( + "SelectTestDao.java", + "InsertTestDao.java", + "UpdateTestDao.java", + "DeleteTestDao.java", + "BatchInsertTestDao.java", + "BatchUpdateTestDao.java", + "BatchDeleteTestDao.java", + "ScriptTestDao.java", + "SqlProcessorTestDao.java", + ) + addResourceFile( + "SelectTestDao/existsSQLFile.sql", + "InsertTestDao/existsSQLFile.sql", + "UpdateTestDao/existsSQLFile.sql", + "DeleteTestDao/existsSQLFile.sql", + "BatchInsertTestDao/existsSQLFile.sql", + "BatchUpdateTestDao/existsSQLFile.sql", + "BatchDeleteTestDao/existsSQLFile.sql", + "ScriptTestDao/existsSQLFile.script", + "SqlProcessorTestDao/existsSQLFile.sql", + ) + myFixture.enableInspections(SqlFileExistInspector()) + } + + fun testSelectExistsSQLFile() { + val dao = findDaoClass("SelectTestDao") + myFixture.testHighlighting( + false, + false, + false, + dao.containingFile.virtualFile, + ) + } + + fun testInsertExistsSQLFile() { + val dao = findDaoClass("InsertTestDao") + myFixture.testHighlighting( + false, + false, + false, + dao.containingFile.virtualFile, + ) + } + + fun testUpdateExistsSQLFile() { + val dao = findDaoClass("UpdateTestDao") + myFixture.testHighlighting( + false, + false, + false, + dao.containingFile.virtualFile, + ) + } + + fun testDeleteExistsSQLFile() { + val dao = findDaoClass("DeleteTestDao") + myFixture.testHighlighting( + false, + false, + false, + dao.containingFile.virtualFile, + ) + } + + fun testBatchInsertExistsSQLFile() { + val dao = findDaoClass("BatchInsertTestDao") + myFixture.testHighlighting( + false, + false, + false, + dao.containingFile.virtualFile, + ) + } + + fun testBatchUpdateExistsSQLFile() { + val dao = findDaoClass("BatchUpdateTestDao") + myFixture.testHighlighting( + false, + false, + false, + dao.containingFile.virtualFile, + ) + } + + fun testBatchDeleteExistsSQLFile() { + val dao = findDaoClass("BatchDeleteTestDao") + myFixture.testHighlighting( + false, + false, + false, + dao.containingFile.virtualFile, + ) + } + + fun testSqlProcessorExistsSQLFile() { + val dao = findDaoClass("SqlProcessorTestDao") + myFixture.testHighlighting( + false, + false, + false, + dao.containingFile.virtualFile, + ) + } + + fun testScriptExistsSQLFile() { + val dao = findDaoClass("ScriptTestDao") + myFixture.testHighlighting( + false, + false, + false, + dao.containingFile.virtualFile, + ) + } +} diff --git a/src/test/kotlin/org/domaframework/doma/intellij/inspection/dao/DomaSqlQuickFixTest.kt b/src/test/kotlin/org/domaframework/doma/intellij/inspection/dao/DomaSqlQuickFixTest.kt new file mode 100644 index 00000000..37ec50cf --- /dev/null +++ b/src/test/kotlin/org/domaframework/doma/intellij/inspection/dao/DomaSqlQuickFixTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.inspection.dao + +import org.domaframework.doma.intellij.DomaSqlTest +import org.domaframework.doma.intellij.bundle.MessageBundle +import org.domaframework.doma.intellij.inspection.dao.inspector.SqlFileExistInspector + +/** + * Quick fix execution test + */ +class DomaSqlQuickFixTest : DomaSqlTest() { + override fun setUp() { + super.setUp() + addDaoJavaFile( + "quickfix/SelectQuickFixTestDao.java", + "quickfix/InsertQuickFixTestDao.java", + "quickfix/UpdateQuickFixTestDao.java", + "quickfix/DeleteQuickFixTestDao.java", + "quickfix/BatchInsertQuickFixTestDao.java", + "quickfix/BatchUpdateQuickFixTestDao.java", + "quickfix/BatchDeleteQuickFixTestDao.java", + "quickfix/ScriptQuickFixTestDao.java", + "quickfix/SqlProcessorQuickFixTestDao.java", + ) + + addResourceFile( + "quickfix/SelectQuickFixTestDao/existsSQLFile.sql", + "quickfix/InsertQuickFixTestDao/existsSQLFile.sql", + "quickfix/UpdateQuickFixTestDao/existsSQLFile.sql", + "quickfix/DeleteQuickFixTestDao/existsSQLFile.sql", + "quickfix/BatchInsertQuickFixTestDao/existsSQLFile.sql", + "quickfix/BatchUpdateQuickFixTestDao/existsSQLFile.sql", + "quickfix/BatchDeleteQuickFixTestDao/existsSQLFile.sql", + "quickfix/ScriptQuickFixTestDao/existsSQLFile.script", + "quickfix/SqlProcessorQuickFixTestDao/existsSQLFile.sql", + ) + myFixture.enableInspections(SqlFileExistInspector()) + } + + fun testSelectGenerateSQLFileQuickFix() { + val testDaoName = "SelectQuickFixTestDao" + daoQuickFixTest(testDaoName, false) { + highlightingDao(testDaoName) + } + } + + fun testInsertGenerateSQLFileQuickFix() { + val testDaoName = "InsertQuickFixTestDao" + daoQuickFixTest(testDaoName, false) { highlightingDao(testDaoName) } + } + + fun testUpdateGenerateSQLFileQuickFix() { + val testDaoName = "UpdateQuickFixTestDao" + daoQuickFixTest(testDaoName, false) { highlightingDao(testDaoName) } + } + + fun testDeleteGenerateSQLFileQuickFix() { + val testDaoName = "DeleteQuickFixTestDao" + daoQuickFixTest(testDaoName, false) { highlightingDao(testDaoName) } + } + + fun testBatchInsertGenerateSQLFileQuickFix() { + val testDaoName = "BatchInsertQuickFixTestDao" + daoQuickFixTest(testDaoName, false) { highlightingDao(testDaoName) } + } + + fun testBatchUpdateGenerateSQLFileQuickFix() { + val testDaoName = "BatchUpdateQuickFixTestDao" + daoQuickFixTest(testDaoName, false) { highlightingDao(testDaoName) } + } + + fun testBatchDeleteGenerateSQLFileQuickFix() { + val testDaoName = "BatchDeleteQuickFixTestDao" + daoQuickFixTest(testDaoName, false) { highlightingDao(testDaoName) } + } + + fun testScriptGenerateSQLFileQuickFix() { + val testDaoName = "ScriptQuickFixTestDao" + daoQuickFixTest(testDaoName, true) { highlightingDao(testDaoName) } + } + + fun testSqlProcessorGenerateSQLFileQuickFix() { + val testDaoName = "SqlProcessorQuickFixTestDao" + daoQuickFixTest(testDaoName, false) { highlightingDao(testDaoName) } + } + + private fun highlightingDao(testDaoName: String) { + val dao = findDaoClass(getQuickFixTestDaoName(testDaoName).replace("/", ".")) + myFixture.testHighlighting( + false, + false, + false, + dao.containingFile.virtualFile, + ) + } + + private fun daoQuickFixTest( + testDaoName: String, + isScript: Boolean, + afterCheck: () -> Unit, + ) { + val quickFixDaoName = getQuickFixTestDaoName(testDaoName) + val dao = findDaoClass(quickFixDaoName.replace("/", ".")) + + myFixture.configureFromExistingVirtualFile(dao.containingFile.virtualFile) + val intention = myFixture.findSingleIntention(MessageBundle.message("generate.sql.quickfix.title")) + myFixture.launchAction(intention) + + val generatedSql = + findSqlFile("$quickFixDaoName/generateSQLFile.${if (isScript) "script" else "sql"}") + assertTrue("Not Found SQL File", generatedSql != null) + + afterCheck() + } + + private fun getQuickFixTestDaoName(daoName: String): String = "quickfix/$daoName" +} diff --git a/src/test/kotlin/org/domaframework/doma/intellij/inspection/dao/DomaUseVariableTest.kt b/src/test/kotlin/org/domaframework/doma/intellij/inspection/dao/DomaUseVariableTest.kt new file mode 100644 index 00000000..2fd84d21 --- /dev/null +++ b/src/test/kotlin/org/domaframework/doma/intellij/inspection/dao/DomaUseVariableTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.inspection.dao + +import org.domaframework.doma.intellij.DomaSqlTest +import org.domaframework.doma.intellij.inspection.dao.inspector.DaoMethodVariableInspector + +/** + * Test class to verify whether Dao method arguments are used + */ +class DomaUseVariableTest : DomaSqlTest() { + private val testDaoName = "DaoMethodVariableInspectionTestDao" + + override fun setUp() { + super.setUp() + addDaoJavaFile( + "$testDaoName.java", + ) + addSqlFile( + "$testDaoName/biFunctionDoesNotCauseError.sql", + "$testDaoName/selectOptionDoesNotCauseError.sql", + ) + myFixture.enableInspections(DaoMethodVariableInspector()) + } + + /** + * Test to verify if Dao method arguments are used + */ + fun testDaoMethodArgumentsUsed() { + val dao = findDaoClass(testDaoName) + myFixture.testHighlighting( + false, + false, + false, + dao.containingFile.virtualFile, + ) + } +} diff --git a/src/test/kotlin/org/domaframework/doma/intellij/inspection/sql/ParameterDefinedTest.kt b/src/test/kotlin/org/domaframework/doma/intellij/inspection/sql/ParameterDefinedTest.kt new file mode 100644 index 00000000..b26f1868 --- /dev/null +++ b/src/test/kotlin/org/domaframework/doma/intellij/inspection/sql/ParameterDefinedTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.inspection.sql + +import org.domaframework.doma.intellij.DomaSqlTest +import org.domaframework.doma.intellij.inspection.sql.inspector.SqlBindVariableValidInspector + +/** + * A test that inspects whether a bind variable's parameters are defined. + */ +class ParameterDefinedTest : DomaSqlTest() { + override fun setUp() { + super.setUp() + addDaoJavaFile( + "EmployeeSummaryDao.java", + ) + addSqlFile( + "EmployeeSummaryDao/bindVariableForEntityAndNonEntityParentClass.sql", + "EmployeeSummaryDao/bindVariableForNonEntityClass.sql", + "EmployeeSummaryDao/accessStaticProperty.sql", + "EmployeeSummaryDao/batchAnnotationResolvesClassInList.sql", + "EmployeeSummaryDao/resolveDaoArgumentOfListType.sql", + ) + myFixture.enableInspections(SqlBindVariableValidInspector()) + } + + /** + * Entity class instance field, method reference test + * + Non-Entity parent class field, method reference test + */ + fun testBindVariableForEntityAndNonEntityParentClass() { + val sqlFile = findSqlFile("EmployeeSummaryDao/bindVariableForEntityAndNonEntityParentClass.sql") + assertNotNull("Not Found SQL File", sqlFile) + if (sqlFile == null) return + + myFixture.testHighlighting(false, false, false, sqlFile) + } + + fun testBindVariableForNonEntityClass() { + val sqlFile = findSqlFile("EmployeeSummaryDao/bindVariableForNonEntityClass.sql") + assertNotNull("Not Found SQL File", sqlFile) + if (sqlFile == null) return + + myFixture.testHighlighting(false, false, false, sqlFile) + } + + fun testAccessStaticProperty() { + val sqlFile = findSqlFile("EmployeeSummaryDao/accessStaticProperty.sql") + assertNotNull("Not Found SQL File", sqlFile) + if (sqlFile == null) return + + myFixture.testHighlighting(false, false, false, sqlFile) + } + + fun testBatchAnnotationResolvesClassInList() { + val sqlFile = + findSqlFile("EmployeeSummaryDao/batchAnnotationResolvesClassInList.sql") + assertNotNull("Not Found SQL File", sqlFile) + if (sqlFile == null) return + + myFixture.testHighlighting(false, false, false, sqlFile) + } + + fun testResolveDaoArgumentOfListType() { + val sqlFile = + findSqlFile("EmployeeSummaryDao/resolveDaoArgumentOfListType.sql") + assertNotNull("Not Found SQL File", sqlFile) + if (sqlFile == null) return + + myFixture.testHighlighting(false, false, false, sqlFile) + } +} diff --git a/src/test/kotlin/org/domaframework/doma/intellij/parsar/DomaParsingTestCase.kt b/src/test/kotlin/org/domaframework/doma/intellij/parsar/DomaParsingTestCase.kt new file mode 100644 index 00000000..0555466f --- /dev/null +++ b/src/test/kotlin/org/domaframework/doma/intellij/parsar/DomaParsingTestCase.kt @@ -0,0 +1,32 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.parsar + +import com.intellij.testFramework.ParsingTestCase +import org.domaframework.doma.intellij.setting.SqlParserDefinition + +/** + * SQL parser test processing as custom language + */ +class DomaParsingTestCase : ParsingTestCase("", "sql", SqlParserDefinition()) { + fun testSQLParser() { + doTest(true) + } + + override fun getTestDataPath(): String = "src/test/testData/sql/parser" + + override fun includeRanges(): Boolean = true +} diff --git a/src/test/kotlin/org/domaframework/doma/intellij/refactor/DaoMethodRenameTestCase.kt b/src/test/kotlin/org/domaframework/doma/intellij/refactor/DaoMethodRenameTestCase.kt new file mode 100644 index 00000000..6385c6ff --- /dev/null +++ b/src/test/kotlin/org/domaframework/doma/intellij/refactor/DaoMethodRenameTestCase.kt @@ -0,0 +1,86 @@ +/* + * Copyright Doma Tools Authors + * + * 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. + */ +package org.domaframework.doma.intellij.refactor + +import org.domaframework.doma.intellij.DomaSqlTest + +/** + * Refactoring test when changing Dao class name and method name + */ +class DaoMethodRenameTestCase : DomaSqlTest() { + override fun setUp() { + super.setUp() + addDaoJavaFile( + "RenameDao.java", + "RenameDaoMethod.java", + "RenameDaoMethodWithoutSql.java", + "RenameDaoMethodNotExistSql.java", + ) + addResourceFile( + "RenameDaoMethod/renameDaoMethodName.sql", + "RenameDao/renameDaoClassName.sql", + ) + } + + fun testRenameDaoClassName() { + val dao = findDaoClass("RenameDao") + myFixture.configureFromExistingVirtualFile(dao.containingFile.virtualFile) + myFixture.renameElementAtCaret("RenameDaoAfter") + myFixture.checkResultByFile("/java/$packagePath/dao/RenameDaoAfter.java", false) + + val afterSqlFile = findSqlFile("RenameDaoAfter/renameDaoClassName.sql") + val beforeSqlFile = findSqlFile("RenameDao/renameDaoClassName.sql") + + assertTrue("No Found SQL File", afterSqlFile != null) + assertFalse("Exist SQL File", beforeSqlFile != null) + } + + fun testRenameDaoMethod() { + val dao = findDaoClass("RenameDaoMethod") + myFixture.configureFromExistingVirtualFile(dao.containingFile.virtualFile) + + myFixture.renameElementAtCaret("renameDaoMethodNameAfter") + myFixture.checkResultByFile("/java/$packagePath/dao/RenameDaoMethodAfter.java", false) + + val afterSqlFile = findSqlFile("RenameDaoMethod/renameDaoMethodNameAfter.sql") + val beforeSqlFile = findSqlFile("RenameDaoMethod/renameDaoMethodName.sql") + + assertTrue("No Found SQL File", afterSqlFile != null) + assertFalse("Exist SQL File", beforeSqlFile != null) + } + + fun testNonRequireSQLFileRenameMethodName() { + val dao = findDaoClass("RenameDaoMethodWithoutSql") + myFixture.configureFromExistingVirtualFile(dao.containingFile.virtualFile) + + myFixture.renameElementAtCaret("notExistSqlAfter") + myFixture.checkResultByFile( + "/java/$packagePath/dao/RenameDaoMethodWithoutSqlAfter.java", + false, + ) + } + + fun testNonExistsSQLFileRenameMethodName() { + val dao = findDaoClass("RenameDaoMethodNotExistSql") + myFixture.configureFromExistingVirtualFile(dao.containingFile.virtualFile) + + myFixture.renameElementAtCaret("renameDaoMethodNameAfter") + myFixture.checkResultByFile( + "/java/$packagePath/dao/RenameDaoMethodNotExistSqlAfter.java", + false, + ) + } +} diff --git a/src/test/lib/doma-core-3.2.0.jar b/src/test/lib/doma-core-3.2.0.jar new file mode 100644 index 00000000..58fc8f93 Binary files /dev/null and b/src/test/lib/doma-core-3.2.0.jar differ diff --git a/src/test/lib/doma-processor-3.2.0.jar b/src/test/lib/doma-processor-3.2.0.jar new file mode 100644 index 00000000..7aa25afe Binary files /dev/null and b/src/test/lib/doma-processor-3.2.0.jar differ diff --git a/src/test/testData/sql/parser/SQLParser.sql b/src/test/testData/sql/parser/SQLParser.sql new file mode 100644 index 00000000..0d3ba892 --- /dev/null +++ b/src/test/testData/sql/parser/SQLParser.sql @@ -0,0 +1,40 @@ +-- Psi tree comparison test data for keywords, directives, bind variables, member accesses, and static property calls +SELECT + e.employee_id + , u.user_id + , u.user_name + , u.email + , e.department + , COUNT(pe.project_id) + , /* employee.numberOfProjects * 10 */ AS projectPoint +FROM user u +JOIN employee e + ON u.user_id = e.user_id +LEFT JOIN project_employee pe + ON e.employee_id = pe.employee_id +WHERE + e.user_id = /* user.userId */0 + AND e.employee_id = /* employee.employeeId */0 + AND e.join_date <= /* referenceDate */'2099/12/31' +/*%if @isNotBlank(employee.departmentId) */ + /*%if employee.departmentId.startsWith("200") */ +  AND e.department_id = /* employee.departmentId */'dept' + /*%elseif employee.numberOfProjects >= 3 */ + AND pe.start_date <= CURRENT_DATE + AND pe.end_date >= CURRENT_DATE + /*%end*/ + /*%for child : employee.departmentId */ + /*%if child_has_next */ + AND pe.parent_project = /* child.projectId */0 + /*%for p : child.member */ + AND pe.type = /* @example.entity.StaticType@PARAM1.getValue() */'0' + /*%end */ + /*%end */ + /*%end */ + /*%end */ +GROUP BY + e.employee_id, + u.user_id, + u.user_name, + u.email, + e.department \ No newline at end of file diff --git a/src/test/testData/sql/parser/SQLParser.txt b/src/test/testData/sql/parser/SQLParser.txt new file mode 100644 index 00000000..81ad6deb --- /dev/null +++ b/src/test/testData/sql/parser/SQLParser.txt @@ -0,0 +1,417 @@ +SQL File(0,1259) + PsiElement(SqlTokenType.LINE_COMMENT)('-- Psi tree comparison test data for keywords, directives, bind variables, member accesses, and static property calls\n')(0,118) + PsiElement(SqlTokenType.KEYWORD)('SELECT')(118,124) + PsiWhiteSpace('\n ')(124,129) + PsiElement(SqlTokenType.WORD)('e')(129,130) + PsiElement(SqlTokenType.OTHER)('.')(130,131) + PsiElement(SqlTokenType.WORD)('employee_id')(131,142) + PsiWhiteSpace('\n ')(142,147) + PsiElement(SqlTokenType.OTHER)(',')(147,148) + PsiWhiteSpace(' ')(148,149) + PsiElement(SqlTokenType.WORD)('u')(149,150) + PsiElement(SqlTokenType.OTHER)('.')(150,151) + PsiElement(SqlTokenType.WORD)('user_id')(151,158) + PsiWhiteSpace('\n ')(158,163) + PsiElement(SqlTokenType.OTHER)(',')(163,164) + PsiWhiteSpace(' ')(164,165) + PsiElement(SqlTokenType.WORD)('u')(165,166) + PsiElement(SqlTokenType.OTHER)('.')(166,167) + PsiElement(SqlTokenType.WORD)('user_name')(167,176) + PsiWhiteSpace('\n ')(176,181) + PsiElement(SqlTokenType.OTHER)(',')(181,182) + PsiWhiteSpace(' ')(182,183) + PsiElement(SqlTokenType.WORD)('u')(183,184) + PsiElement(SqlTokenType.OTHER)('.')(184,185) + PsiElement(SqlTokenType.WORD)('email')(185,190) + PsiWhiteSpace('\n ')(190,195) + PsiElement(SqlTokenType.OTHER)(',')(195,196) + PsiWhiteSpace(' ')(196,197) + PsiElement(SqlTokenType.WORD)('e')(197,198) + PsiElement(SqlTokenType.OTHER)('.')(198,199) + PsiElement(SqlTokenType.WORD)('department')(199,209) + PsiWhiteSpace('\n ')(209,214) + PsiElement(SqlTokenType.OTHER)(',')(214,215) + PsiWhiteSpace(' ')(215,216) + PsiElement(SqlTokenType.WORD)('COUNT')(216,221) + PsiElement(SqlTokenType.OTHER)('(')(221,222) + PsiElement(SqlTokenType.WORD)('pe')(222,224) + PsiElement(SqlTokenType.OTHER)('.')(224,225) + PsiElement(SqlTokenType.WORD)('project_id')(225,235) + PsiElement(SqlTokenType.OTHER)(')')(235,236) + PsiWhiteSpace('\n ')(236,241) + PsiElement(SqlTokenType.OTHER)(',')(241,242) + PsiWhiteSpace(' ')(242,243) + SqlBlockCommentImpl(BLOCK_COMMENT)(243,279) + PsiElement(SqlTokenType./*)('/*')(243,245) + PsiWhiteSpace(' ')(245,246) + SqlElMultiplyExprImpl(EL_MULTIPLY_EXPR)(246,276) + SqlElFieldAccessExprImpl(EL_FIELD_ACCESS_EXPR)(246,271) + SqlElPrimaryExprImpl(EL_PRIMARY_EXPR)(246,254) + PsiElement(SqlTokenType.EL_IDENTIFIER)('employee')(246,254) + PsiElement(SqlTokenType..)('.')(254,255) + PsiElement(SqlTokenType.EL_IDENTIFIER)('numberOfProjects')(255,271) + PsiWhiteSpace(' ')(271,272) + PsiElement(SqlTokenType.*)('*')(272,273) + PsiWhiteSpace(' ')(273,274) + SqlElPrimaryExprImpl(EL_PRIMARY_EXPR)(274,276) + PsiElement(SqlTokenType.EL_NUMBER)('10')(274,276) + PsiWhiteSpace(' ')(276,277) + PsiElement(SqlTokenType.*/)('*/')(277,279) + PsiWhiteSpace(' ')(279,280) + PsiElement(SqlTokenType.WORD)('AS')(280,282) + PsiWhiteSpace(' ')(282,283) + PsiElement(SqlTokenType.WORD)('projectPoint')(283,295) + PsiWhiteSpace('\n')(295,296) + PsiElement(SqlTokenType.KEYWORD)('FROM')(296,300) + PsiWhiteSpace(' ')(300,301) + PsiElement(SqlTokenType.WORD)('user')(301,305) + PsiWhiteSpace(' ')(305,306) + PsiElement(SqlTokenType.WORD)('u')(306,307) + PsiWhiteSpace('\n')(307,308) + PsiElement(SqlTokenType.KEYWORD)('JOIN')(308,312) + PsiWhiteSpace(' ')(312,313) + PsiElement(SqlTokenType.WORD)('employee')(313,321) + PsiWhiteSpace(' ')(321,322) + PsiElement(SqlTokenType.WORD)('e')(322,323) + PsiWhiteSpace('\n ')(323,328) + PsiElement(SqlTokenType.KEYWORD)('ON')(328,330) + PsiWhiteSpace(' ')(330,331) + PsiElement(SqlTokenType.WORD)('u')(331,332) + PsiElement(SqlTokenType.OTHER)('.')(332,333) + PsiElement(SqlTokenType.WORD)('user_id')(333,340) + PsiWhiteSpace(' ')(340,341) + PsiElement(SqlTokenType.OTHER)('=')(341,342) + PsiWhiteSpace(' ')(342,343) + PsiElement(SqlTokenType.WORD)('e')(343,344) + PsiElement(SqlTokenType.OTHER)('.')(344,345) + PsiElement(SqlTokenType.WORD)('user_id')(345,352) + PsiWhiteSpace('\n')(352,353) + PsiElement(SqlTokenType.KEYWORD)('LEFT')(353,357) + PsiWhiteSpace(' ')(357,358) + PsiElement(SqlTokenType.KEYWORD)('JOIN')(358,362) + PsiWhiteSpace(' ')(362,363) + PsiElement(SqlTokenType.WORD)('project_employee')(363,379) + PsiWhiteSpace(' ')(379,380) + PsiElement(SqlTokenType.WORD)('pe')(380,382) + PsiWhiteSpace('\n ')(382,387) + PsiElement(SqlTokenType.KEYWORD)('ON')(387,389) + PsiWhiteSpace(' ')(389,390) + PsiElement(SqlTokenType.WORD)('e')(390,391) + PsiElement(SqlTokenType.OTHER)('.')(391,392) + PsiElement(SqlTokenType.WORD)('employee_id')(392,403) + PsiWhiteSpace(' ')(403,404) + PsiElement(SqlTokenType.OTHER)('=')(404,405) + PsiWhiteSpace(' ')(405,406) + PsiElement(SqlTokenType.WORD)('pe')(406,408) + PsiElement(SqlTokenType.OTHER)('.')(408,409) + PsiElement(SqlTokenType.WORD)('employee_id')(409,420) + PsiWhiteSpace('\n')(420,421) + PsiElement(SqlTokenType.KEYWORD)('WHERE')(421,426) + PsiWhiteSpace('\n ')(426,431) + PsiElement(SqlTokenType.WORD)('e')(431,432) + PsiElement(SqlTokenType.OTHER)('.')(432,433) + PsiElement(SqlTokenType.WORD)('user_id')(433,440) + PsiWhiteSpace(' ')(440,441) + PsiElement(SqlTokenType.OTHER)('=')(441,442) + PsiWhiteSpace(' ')(442,443) + SqlBlockCommentImpl(BLOCK_COMMENT)(443,460) + PsiElement(SqlTokenType./*)('/*')(443,445) + PsiWhiteSpace(' ')(445,446) + SqlElFieldAccessExprImpl(EL_FIELD_ACCESS_EXPR)(446,457) + SqlElPrimaryExprImpl(EL_PRIMARY_EXPR)(446,450) + PsiElement(SqlTokenType.EL_IDENTIFIER)('user')(446,450) + PsiElement(SqlTokenType..)('.')(450,451) + PsiElement(SqlTokenType.EL_IDENTIFIER)('userId')(451,457) + PsiWhiteSpace(' ')(457,458) + PsiElement(SqlTokenType.*/)('*/')(458,460) + PsiElement(SqlTokenType.NUMBER)('0')(460,461) + PsiWhiteSpace('\n ')(461,466) + PsiElement(SqlTokenType.KEYWORD)('AND')(466,469) + PsiWhiteSpace(' ')(469,470) + PsiElement(SqlTokenType.WORD)('e')(470,471) + PsiElement(SqlTokenType.OTHER)('.')(471,472) + PsiElement(SqlTokenType.WORD)('employee_id')(472,483) + PsiWhiteSpace(' ')(483,484) + PsiElement(SqlTokenType.OTHER)('=')(484,485) + PsiWhiteSpace(' ')(485,486) + SqlBlockCommentImpl(BLOCK_COMMENT)(486,511) + PsiElement(SqlTokenType./*)('/*')(486,488) + PsiWhiteSpace(' ')(488,489) + SqlElFieldAccessExprImpl(EL_FIELD_ACCESS_EXPR)(489,508) + SqlElPrimaryExprImpl(EL_PRIMARY_EXPR)(489,497) + PsiElement(SqlTokenType.EL_IDENTIFIER)('employee')(489,497) + PsiElement(SqlTokenType..)('.')(497,498) + PsiElement(SqlTokenType.EL_IDENTIFIER)('employeeId')(498,508) + PsiWhiteSpace(' ')(508,509) + PsiElement(SqlTokenType.*/)('*/')(509,511) + PsiElement(SqlTokenType.NUMBER)('0')(511,512) + PsiWhiteSpace('\n ')(512,517) + PsiElement(SqlTokenType.KEYWORD)('AND')(517,520) + PsiWhiteSpace(' ')(520,521) + PsiElement(SqlTokenType.WORD)('e')(521,522) + PsiElement(SqlTokenType.OTHER)('.')(522,523) + PsiElement(SqlTokenType.WORD)('join_date')(523,532) + PsiWhiteSpace(' ')(532,533) + PsiElement(SqlTokenType.OTHER)('<')(533,534) + PsiElement(SqlTokenType.OTHER)('=')(534,535) + PsiWhiteSpace(' ')(535,536) + SqlBlockCommentImpl(BLOCK_COMMENT)(536,555) + PsiElement(SqlTokenType./*)('/*')(536,538) + PsiWhiteSpace(' ')(538,539) + SqlElPrimaryExprImpl(EL_PRIMARY_EXPR)(539,552) + PsiElement(SqlTokenType.EL_IDENTIFIER)('referenceDate')(539,552) + PsiWhiteSpace(' ')(552,553) + PsiElement(SqlTokenType.*/)('*/')(553,555) + PsiElement(SqlTokenType.STRING)(''2099/12/31'')(555,567) + PsiWhiteSpace('\n')(567,568) + SqlBlockCommentImpl(BLOCK_COMMENT)(568,611) + PsiElement(SqlTokenType./*)('/*')(568,570) + SqlElIfDirectiveImpl(EL_IF_DIRECTIVE)(570,608) + PsiElement(SqlTokenType.%if)('%if')(570,573) + PsiWhiteSpace(' ')(573,574) + SqlElFunctionCallExprImpl(EL_FUNCTION_CALL_EXPR)(574,608) + PsiElement(SqlTokenType.@)('@')(574,575) + PsiElement(SqlTokenType.EL_IDENTIFIER)('isNotBlank')(575,585) + SqlElParametersImpl(EL_PARAMETERS)(585,608) + PsiElement(SqlTokenType.()('(')(585,586) + SqlElFieldAccessExprImpl(EL_FIELD_ACCESS_EXPR)(586,607) + SqlElPrimaryExprImpl(EL_PRIMARY_EXPR)(586,594) + PsiElement(SqlTokenType.EL_IDENTIFIER)('employee')(586,594) + PsiElement(SqlTokenType..)('.')(594,595) + PsiElement(SqlTokenType.EL_IDENTIFIER)('departmentId')(595,607) + PsiElement(SqlTokenType.))(')')(607,608) + PsiWhiteSpace(' ')(608,609) + PsiElement(SqlTokenType.*/)('*/')(609,611) + PsiWhiteSpace('\n ')(611,616) + SqlBlockCommentImpl(BLOCK_COMMENT)(616,664) + PsiElement(SqlTokenType./*)('/*')(616,618) + SqlElIfDirectiveImpl(EL_IF_DIRECTIVE)(618,661) + PsiElement(SqlTokenType.%if)('%if')(618,621) + PsiWhiteSpace(' ')(621,622) + SqlElFieldAccessExprImpl(EL_FIELD_ACCESS_EXPR)(622,661) + SqlElPrimaryExprImpl(EL_PRIMARY_EXPR)(622,630) + PsiElement(SqlTokenType.EL_IDENTIFIER)('employee')(622,630) + PsiElement(SqlTokenType..)('.')(630,631) + PsiElement(SqlTokenType.EL_IDENTIFIER)('departmentId')(631,643) + PsiElement(SqlTokenType..)('.')(643,644) + PsiElement(SqlTokenType.EL_IDENTIFIER)('startsWith')(644,654) + SqlElParametersImpl(EL_PARAMETERS)(654,661) + PsiElement(SqlTokenType.()('(')(654,655) + SqlElPrimaryExprImpl(EL_PRIMARY_EXPR)(655,660) + PsiElement(SqlTokenType.EL_STRING)('"200"')(655,660) + PsiElement(SqlTokenType.))(')')(660,661) + PsiWhiteSpace(' ')(661,662) + PsiElement(SqlTokenType.*/)('*/')(662,664) + PsiWhiteSpace('\n ')(664,671) + PsiElement(SqlTokenType.OTHER)(' ')(671,672) + PsiElement(SqlTokenType.KEYWORD)('AND')(672,675) + PsiWhiteSpace(' ')(675,676) + PsiElement(SqlTokenType.WORD)('e')(676,677) + PsiElement(SqlTokenType.OTHER)('.')(677,678) + PsiElement(SqlTokenType.WORD)('department_id')(678,691) + PsiWhiteSpace(' ')(691,692) + PsiElement(SqlTokenType.OTHER)('=')(692,693) + PsiWhiteSpace(' ')(693,694) + SqlBlockCommentImpl(BLOCK_COMMENT)(694,721) + PsiElement(SqlTokenType./*)('/*')(694,696) + PsiWhiteSpace(' ')(696,697) + SqlElFieldAccessExprImpl(EL_FIELD_ACCESS_EXPR)(697,718) + SqlElPrimaryExprImpl(EL_PRIMARY_EXPR)(697,705) + PsiElement(SqlTokenType.EL_IDENTIFIER)('employee')(697,705) + PsiElement(SqlTokenType..)('.')(705,706) + PsiElement(SqlTokenType.EL_IDENTIFIER)('departmentId')(706,718) + PsiWhiteSpace(' ')(718,719) + PsiElement(SqlTokenType.*/)('*/')(719,721) + PsiElement(SqlTokenType.STRING)(''dept'')(721,727) + PsiWhiteSpace('\n ')(727,732) + SqlBlockCommentImpl(BLOCK_COMMENT)(732,775) + PsiElement(SqlTokenType./*)('/*')(732,734) + SqlElElseifDirectiveImpl(EL_ELSEIF_DIRECTIVE)(734,772) + PsiElement(SqlTokenType.%elseif)('%elseif')(734,741) + PsiWhiteSpace(' ')(741,742) + SqlElGeExprImpl(EL_GE_EXPR)(742,772) + SqlElFieldAccessExprImpl(EL_FIELD_ACCESS_EXPR)(742,767) + SqlElPrimaryExprImpl(EL_PRIMARY_EXPR)(742,750) + PsiElement(SqlTokenType.EL_IDENTIFIER)('employee')(742,750) + PsiElement(SqlTokenType..)('.')(750,751) + PsiElement(SqlTokenType.EL_IDENTIFIER)('numberOfProjects')(751,767) + PsiWhiteSpace(' ')(767,768) + PsiElement(SqlTokenType.>=)('>=')(768,770) + PsiWhiteSpace(' ')(770,771) + SqlElPrimaryExprImpl(EL_PRIMARY_EXPR)(771,772) + PsiElement(SqlTokenType.EL_NUMBER)('3')(771,772) + PsiWhiteSpace(' ')(772,773) + PsiElement(SqlTokenType.*/)('*/')(773,775) + PsiWhiteSpace('\n ')(775,784) + PsiElement(SqlTokenType.KEYWORD)('AND')(784,787) + PsiWhiteSpace(' ')(787,788) + PsiElement(SqlTokenType.WORD)('pe')(788,790) + PsiElement(SqlTokenType.OTHER)('.')(790,791) + PsiElement(SqlTokenType.WORD)('start_date')(791,801) + PsiWhiteSpace(' ')(801,802) + PsiElement(SqlTokenType.OTHER)('<')(802,803) + PsiElement(SqlTokenType.OTHER)('=')(803,804) + PsiWhiteSpace(' ')(804,805) + PsiElement(SqlTokenType.WORD)('CURRENT_DATE')(805,817) + PsiWhiteSpace('\n ')(817,826) + PsiElement(SqlTokenType.KEYWORD)('AND')(826,829) + PsiWhiteSpace(' ')(829,830) + PsiElement(SqlTokenType.WORD)('pe')(830,832) + PsiElement(SqlTokenType.OTHER)('.')(832,833) + PsiElement(SqlTokenType.WORD)('end_date')(833,841) + PsiWhiteSpace(' ')(841,842) + PsiElement(SqlTokenType.OTHER)('>')(842,843) + PsiElement(SqlTokenType.OTHER)('=')(843,844) + PsiWhiteSpace(' ')(844,845) + PsiElement(SqlTokenType.WORD)('CURRENT_DATE')(845,857) + PsiWhiteSpace('\n ')(857,862) + SqlBlockCommentImpl(BLOCK_COMMENT)(862,870) + PsiElement(SqlTokenType./*)('/*')(862,864) + PsiElement(SqlTokenType.%end)('%end')(864,868) + PsiElement(SqlTokenType.*/)('*/')(868,870) + PsiWhiteSpace('\n ')(870,875) + SqlBlockCommentImpl(BLOCK_COMMENT)(875,914) + PsiElement(SqlTokenType./*)('/*')(875,877) + SqlElForDirectiveImpl(EL_FOR_DIRECTIVE)(877,911) + PsiElement(SqlTokenType.%for)('%for')(877,881) + PsiWhiteSpace(' ')(881,882) + PsiElement(SqlTokenType.EL_IDENTIFIER)('child')(882,887) + PsiWhiteSpace(' ')(887,888) + PsiElement(SqlTokenType.:)(':')(888,889) + PsiWhiteSpace(' ')(889,890) + SqlElFieldAccessExprImpl(EL_FIELD_ACCESS_EXPR)(890,911) + SqlElPrimaryExprImpl(EL_PRIMARY_EXPR)(890,898) + PsiElement(SqlTokenType.EL_IDENTIFIER)('employee')(890,898) + PsiElement(SqlTokenType..)('.')(898,899) + PsiElement(SqlTokenType.EL_IDENTIFIER)('departmentId')(899,911) + PsiWhiteSpace(' ')(911,912) + PsiElement(SqlTokenType.*/)('*/')(912,914) + PsiWhiteSpace('\n ')(914,922) + SqlBlockCommentImpl(BLOCK_COMMENT)(922,946) + PsiElement(SqlTokenType./*)('/*')(922,924) + SqlElIfDirectiveImpl(EL_IF_DIRECTIVE)(924,943) + PsiElement(SqlTokenType.%if)('%if')(924,927) + PsiWhiteSpace(' ')(927,929) + SqlElPrimaryExprImpl(EL_PRIMARY_EXPR)(929,943) + PsiElement(SqlTokenType.EL_IDENTIFIER)('child_has_next')(929,943) + PsiWhiteSpace(' ')(943,944) + PsiElement(SqlTokenType.*/)('*/')(944,946) + PsiWhiteSpace('\n ')(946,954) + PsiElement(SqlTokenType.KEYWORD)('AND')(954,957) + PsiWhiteSpace(' ')(957,958) + PsiElement(SqlTokenType.WORD)('pe')(958,960) + PsiElement(SqlTokenType.OTHER)('.')(960,961) + PsiElement(SqlTokenType.WORD)('parent_project')(961,975) + PsiWhiteSpace(' ')(975,976) + PsiElement(SqlTokenType.OTHER)('=')(976,977) + PsiWhiteSpace(' ')(977,978) + SqlBlockCommentImpl(BLOCK_COMMENT)(978,999) + PsiElement(SqlTokenType./*)('/*')(978,980) + PsiWhiteSpace(' ')(980,981) + SqlElFieldAccessExprImpl(EL_FIELD_ACCESS_EXPR)(981,996) + SqlElPrimaryExprImpl(EL_PRIMARY_EXPR)(981,986) + PsiElement(SqlTokenType.EL_IDENTIFIER)('child')(981,986) + PsiElement(SqlTokenType..)('.')(986,987) + PsiElement(SqlTokenType.EL_IDENTIFIER)('projectId')(987,996) + PsiWhiteSpace(' ')(996,997) + PsiElement(SqlTokenType.*/)('*/')(997,999) + PsiElement(SqlTokenType.NUMBER)('0')(999,1000) + PsiWhiteSpace('\n ')(1000,1007) + SqlBlockCommentImpl(BLOCK_COMMENT)(1007,1033) + PsiElement(SqlTokenType./*)('/*')(1007,1009) + SqlElForDirectiveImpl(EL_FOR_DIRECTIVE)(1009,1030) + PsiElement(SqlTokenType.%for)('%for')(1009,1013) + PsiWhiteSpace(' ')(1013,1014) + PsiElement(SqlTokenType.EL_IDENTIFIER)('p')(1014,1015) + PsiWhiteSpace(' ')(1015,1016) + PsiElement(SqlTokenType.:)(':')(1016,1017) + PsiWhiteSpace(' ')(1017,1018) + SqlElFieldAccessExprImpl(EL_FIELD_ACCESS_EXPR)(1018,1030) + SqlElPrimaryExprImpl(EL_PRIMARY_EXPR)(1018,1023) + PsiElement(SqlTokenType.EL_IDENTIFIER)('child')(1018,1023) + PsiElement(SqlTokenType..)('.')(1023,1024) + PsiElement(SqlTokenType.EL_IDENTIFIER)('member')(1024,1030) + PsiWhiteSpace(' ')(1030,1031) + PsiElement(SqlTokenType.*/)('*/')(1031,1033) + PsiWhiteSpace('\n ')(1033,1040) + PsiElement(SqlTokenType.KEYWORD)('AND')(1040,1043) + PsiWhiteSpace(' ')(1043,1044) + PsiElement(SqlTokenType.WORD)('pe')(1044,1046) + PsiElement(SqlTokenType.OTHER)('.')(1046,1047) + PsiElement(SqlTokenType.WORD)('type')(1047,1051) + PsiWhiteSpace(' ')(1051,1052) + PsiElement(SqlTokenType.OTHER)('=')(1052,1053) + PsiWhiteSpace(' ')(1053,1054) + SqlBlockCommentImpl(BLOCK_COMMENT)(1054,1104) + PsiElement(SqlTokenType./*)('/*')(1054,1056) + PsiWhiteSpace(' ')(1056,1057) + SqlElStaticFieldAccessExprImpl(EL_STATIC_FIELD_ACCESS_EXPR)(1057,1101) + PsiElement(SqlTokenType.@)('@')(1057,1058) + SqlElClassImpl(EL_CLASS)(1058,1083) + PsiElement(SqlTokenType.EL_IDENTIFIER)('example')(1058,1065) + PsiElement(SqlTokenType..)('.')(1065,1066) + PsiElement(SqlTokenType.EL_IDENTIFIER)('entity')(1066,1072) + PsiElement(SqlTokenType..)('.')(1072,1073) + PsiElement(SqlTokenType.EL_IDENTIFIER)('StaticType')(1073,1083) + PsiElement(SqlTokenType.@)('@')(1083,1084) + PsiElement(SqlTokenType.EL_IDENTIFIER)('PARAM1')(1084,1090) + PsiElement(SqlTokenType..)('.')(1090,1091) + PsiElement(SqlTokenType.EL_IDENTIFIER)('getValue')(1091,1099) + SqlElParametersImpl(EL_PARAMETERS)(1099,1101) + PsiElement(SqlTokenType.()('(')(1099,1100) + PsiElement(SqlTokenType.))(')')(1100,1101) + PsiWhiteSpace(' ')(1101,1102) + PsiElement(SqlTokenType.*/)('*/')(1102,1104) + PsiElement(SqlTokenType.STRING)(''0'')(1104,1107) + PsiWhiteSpace('\n ')(1107,1114) + SqlBlockCommentImpl(BLOCK_COMMENT)(1114,1123) + PsiElement(SqlTokenType./*)('/*')(1114,1116) + PsiElement(SqlTokenType.%end)('%end')(1116,1120) + PsiWhiteSpace(' ')(1120,1121) + PsiElement(SqlTokenType.*/)('*/')(1121,1123) + PsiWhiteSpace('\n ')(1123,1130) + SqlBlockCommentImpl(BLOCK_COMMENT)(1130,1139) + PsiElement(SqlTokenType./*)('/*')(1130,1132) + PsiElement(SqlTokenType.%end)('%end')(1132,1136) + PsiWhiteSpace(' ')(1136,1137) + PsiElement(SqlTokenType.*/)('*/')(1137,1139) + PsiWhiteSpace('\n ')(1139,1144) + SqlBlockCommentImpl(BLOCK_COMMENT)(1144,1153) + PsiElement(SqlTokenType./*)('/*')(1144,1146) + PsiElement(SqlTokenType.%end)('%end')(1146,1150) + PsiWhiteSpace(' ')(1150,1151) + PsiElement(SqlTokenType.*/)('*/')(1151,1153) + PsiWhiteSpace('\n ')(1153,1160) + SqlBlockCommentImpl(BLOCK_COMMENT)(1160,1169) + PsiElement(SqlTokenType./*)('/*')(1160,1162) + PsiElement(SqlTokenType.%end)('%end')(1162,1166) + PsiWhiteSpace(' ')(1166,1167) + PsiElement(SqlTokenType.*/)('*/')(1167,1169) + PsiWhiteSpace('\n')(1169,1170) + PsiElement(SqlTokenType.KEYWORD)('GROUP')(1170,1175) + PsiWhiteSpace(' ')(1175,1176) + PsiElement(SqlTokenType.KEYWORD)('BY')(1176,1178) + PsiWhiteSpace('\n ')(1178,1183) + PsiElement(SqlTokenType.WORD)('e')(1183,1184) + PsiElement(SqlTokenType.OTHER)('.')(1184,1185) + PsiElement(SqlTokenType.WORD)('employee_id')(1185,1196) + PsiElement(SqlTokenType.OTHER)(',')(1196,1197) + PsiWhiteSpace('\n ')(1197,1202) + PsiElement(SqlTokenType.WORD)('u')(1202,1203) + PsiElement(SqlTokenType.OTHER)('.')(1203,1204) + PsiElement(SqlTokenType.WORD)('user_id')(1204,1211) + PsiElement(SqlTokenType.OTHER)(',')(1211,1212) + PsiWhiteSpace('\n ')(1212,1217) + PsiElement(SqlTokenType.WORD)('u')(1217,1218) + PsiElement(SqlTokenType.OTHER)('.')(1218,1219) + PsiElement(SqlTokenType.WORD)('user_name')(1219,1228) + PsiElement(SqlTokenType.OTHER)(',')(1228,1229) + PsiWhiteSpace('\n ')(1229,1234) + PsiElement(SqlTokenType.WORD)('u')(1234,1235) + PsiElement(SqlTokenType.OTHER)('.')(1235,1236) + PsiElement(SqlTokenType.WORD)('email')(1236,1241) + PsiElement(SqlTokenType.OTHER)(',')(1241,1242) + PsiWhiteSpace('\n ')(1242,1247) + PsiElement(SqlTokenType.WORD)('e')(1247,1248) + PsiElement(SqlTokenType.OTHER)('.')(1248,1249) + PsiElement(SqlTokenType.WORD)('department')(1249,1259) diff --git a/src/test/testData/src/main/java/doma/example/dao/BatchDeleteTestDao.java b/src/test/testData/src/main/java/doma/example/dao/BatchDeleteTestDao.java new file mode 100644 index 00000000..951808b5 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/BatchDeleteTestDao.java @@ -0,0 +1,23 @@ +package doma.example.dao; + +import java.util.List; +import doma.example.entity.*; +import org.seasar.doma.*; + +@Dao +public interface BatchDeleteTestDao { + + @BatchDelete + int[] nonExistSQLFile(List employees); + + @BatchDelete(sqlFile = true) + @Sql("delete from employee where id = /* employees.employeeId */0 and name = /* employees.userName */'name'") + int[] nonExistSQLFileAndTemplateIncluded(List employees); + + @BatchDelete(sqlFile = true) + int[] nonExistSQLFileError(List employees); + + @BatchDelete(sqlFile = true) + int[] existsSQLFile(List employees); + +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/BatchInsertTestDao.java b/src/test/testData/src/main/java/doma/example/dao/BatchInsertTestDao.java new file mode 100644 index 00000000..677dcdb3 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/BatchInsertTestDao.java @@ -0,0 +1,23 @@ +package doma.example.dao; + +import java.util.List; +import doma.example.entity.*; +import org.seasar.doma.*; + + +@Dao +interface BatchInsertTestDao { + + @BatchInsert + int[] nonExistSQLFile(List employees); + + @BatchInsert(sqlFile = true) + @Sql("insert into employee (id, name) values (/* employees.employeeId */0, /* employees.name */'name')") + int[] nonExistSQLFileAndTemplateIncluded(List employees); + + @BatchInsert(sqlFile = true) + int[] nonExistSQLFileError(List employees); + + @BatchInsert(sqlFile = true) + int[] existsSQLFile(List employees); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/BatchUpdateTestDao.java b/src/test/testData/src/main/java/doma/example/dao/BatchUpdateTestDao.java new file mode 100644 index 00000000..de9767cb --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/BatchUpdateTestDao.java @@ -0,0 +1,23 @@ +package doma.example.dao; + +import java.util.List; +import doma.example.entity.*; +import org.seasar.doma.*; + + +@Dao +interface BatchUpdateTestDao { + + @BatchUpdate + int[] nonExistSQLFile(List employee); + + @BatchUpdate(sqlFile = true) + @Sql("update employee set id = /* employees.employeeId */0, name = /* employees.userName */'name'") + int[] nonExistSQLFileAndTemplateIncluded(List employees); + + @BatchUpdate(sqlFile = true) + int[] nonExistSQLFileError(List employees); + + @BatchUpdate(sqlFile = true) + int[] existsSQLFile(List employees); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/DaoMethodVariableInspectionTestDao.java b/src/test/testData/src/main/java/doma/example/dao/DaoMethodVariableInspectionTestDao.java new file mode 100644 index 00000000..5e43e509 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/DaoMethodVariableInspectionTestDao.java @@ -0,0 +1,42 @@ +package doma.example.dao; + +import doma.example.entity.*; +import org.seasar.doma.BatchDelete; +import org.seasar.doma.BatchInsert; +import org.seasar.doma.BatchUpdate; +import org.seasar.doma.Dao; +import org.seasar.doma.Delete; +import org.seasar.doma.Insert; +import org.seasar.doma.MultiInsert; +import org.seasar.doma.Script; +import org.seasar.doma.Select; +import org.seasar.doma.Sql; +import org.seasar.doma.SqlProcessor; +import org.seasar.doma.Update; +import org.seasar.doma.jdbc.Config; +import org.seasar.doma.jdbc.PreparedSql; +import org.seasar.doma.jdbc.SelectOptions; + +import java.util.List; +import java.util.function.BiFunction; + +/** + * Test to check for unused arguments in SQL + */ +@Dao +interface DaoMethodVariableInspectionTestDao { + + @Select + List nonExistSQLFile(String name); + + @Select + @Sql("select * from employee where name = /* name */'test'") + Employee noArgumentsUsedInSQLAnnotations(String name,Integer id); + + @SqlProcessor + R biFunctionDoesNotCauseError(Integer id, BiFunction handler); + + @Select + Project selectOptionDoesNotCauseError(Employee employee,String searchName,SelectOptions options); + +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/DeleteTestDao.java b/src/test/testData/src/main/java/doma/example/dao/DeleteTestDao.java new file mode 100644 index 00000000..e3522b94 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/DeleteTestDao.java @@ -0,0 +1,22 @@ +package doma.example.dao; + +import doma.example.entity.*; +import org.seasar.doma.*; + + +@Dao +interface DeleteTestDao { + + @Delete + int nonExistSQLFile(Employee employee); + + @Delete(sqlFile = true) + @Sql("delete from employee where id = /* employee.employeeId */0 and name = /* employee.userName */'name'") + int nonExistSQLFileAndTemplateIncluded(Employee employee); + + @Delete(sqlFile = true) + int nonExistSQLFileError(Employee employee); + + @Delete(sqlFile = true) + int existsSQLFile(Employee employee); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/EmployeeSummaryDao.java b/src/test/testData/src/main/java/doma/example/dao/EmployeeSummaryDao.java new file mode 100644 index 00000000..2fc1f01f --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/EmployeeSummaryDao.java @@ -0,0 +1,29 @@ +package doma.example.dao; + +import doma.example.entity.*: +import org.seasar.doma.*; +import org.seasar.doma.jdbc.Config; +import org.seasar.doma.jdbc.PreparedSql; +import org.seasar.doma.jdbc.SelectOptions; + +import java.util.List; +import java.util.function.BiFunction; + +@Dao +interface EmployeeSummaryDao { + + @Select + EmployeeSummary bindVariableForNonEntityClass(EmployeeSummary employee, User user); + + @Insert(sqlFile=true) + EmployeeSummary bindVariableForEntityAndNonEntityParentClass(Employee employee); + + @Update(sqlFile=true) + int accessStaticProperty(Project project,LocalDate referenceDate); + + @Select + ProjectDetail resolveDaoArgumentOfListType(List employees); + + @BatchInsert(sqlFile=true) + int[] batchAnnotationResolvesClassInList(List employees); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/InsertTestDao.java b/src/test/testData/src/main/java/doma/example/dao/InsertTestDao.java new file mode 100644 index 00000000..997b9edd --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/InsertTestDao.java @@ -0,0 +1,22 @@ +package doma.example.dao; + +import doma.example.entity.*; +import org.seasar.doma.*; + + +@Dao +public interface InsertTestDao { + + @Insert + int nonExistSQLFile(Employee employee); + + @Insert + @Sql("insert into employee (id, name) values (/* employee.employeeId */0, /* employee.name */'name')") + int nonExistSQLFileAndTemplateIncluded(Employee employee); + + @Insert(sqlFile = true) + int nonExistSQLFileError(Employee employee, String orderBy); + + @Insert(sqlFile = true) + int existsSQLFile(Employee employee, String orderBy); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/RenameDao.java b/src/test/testData/src/main/java/doma/example/dao/RenameDao.java new file mode 100644 index 00000000..6d729a03 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/RenameDao.java @@ -0,0 +1,10 @@ +package doma.example.dao; + +import doma.example.entity.*; +import org.seasar.doma.*; + +@Dao +interface RenameDao { + @Select + Employee renameDaoClassName(Integer id,String name); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/RenameDaoAfter.java b/src/test/testData/src/main/java/doma/example/dao/RenameDaoAfter.java new file mode 100644 index 00000000..0415776a --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/RenameDaoAfter.java @@ -0,0 +1,10 @@ +package doma.example.dao; + +import doma.example.entity.*; +import org.seasar.doma.*; + +@Dao +interface RenameDaoAfter { + @Select + Employee renameDaoClassName(Integer id,String name); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/RenameDaoMethod.java b/src/test/testData/src/main/java/doma/example/dao/RenameDaoMethod.java new file mode 100644 index 00000000..82553781 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/RenameDaoMethod.java @@ -0,0 +1,10 @@ +package doma.example.dao; + +import doma.example.entity.*; +import org.seasar.doma.*; + +@Dao +interface RenameDaoMethod { + @Select + Employee renameDaoMethodName(Integer id,String name); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/RenameDaoMethodAfter.java b/src/test/testData/src/main/java/doma/example/dao/RenameDaoMethodAfter.java new file mode 100644 index 00000000..99c98854 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/RenameDaoMethodAfter.java @@ -0,0 +1,10 @@ +package doma.example.dao; + +import doma.example.entity.*; +import org.seasar.doma.*; + +@Dao +interface RenameDaoMethod { + @Select + Employee renameDaoMethodNameAfter(Integer id, String name); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/RenameDaoMethodNotExistSql.java b/src/test/testData/src/main/java/doma/example/dao/RenameDaoMethodNotExistSql.java new file mode 100644 index 00000000..1e02af26 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/RenameDaoMethodNotExistSql.java @@ -0,0 +1,10 @@ +package doma.example.dao; + +import doma.example.entity.*; +import org.seasar.doma.*; + +@Dao +interface RenameDaoMethodNotExistSql { + @Select + Employee renameDaoMethodName(Integer id,String name); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/RenameDaoMethodNotExistSqlAfter.java b/src/test/testData/src/main/java/doma/example/dao/RenameDaoMethodNotExistSqlAfter.java new file mode 100644 index 00000000..d9b93cf0 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/RenameDaoMethodNotExistSqlAfter.java @@ -0,0 +1,10 @@ +package doma.example.dao; + +import doma.example.entity.*; +import org.seasar.doma.*; + +@Dao +interface RenameDaoMethodNotExistSql { + @Select + Employee renameDaoMethodNameAfter(Integer id, String name); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/RenameDaoMethodWithoutSql.java b/src/test/testData/src/main/java/doma/example/dao/RenameDaoMethodWithoutSql.java new file mode 100644 index 00000000..76718d96 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/RenameDaoMethodWithoutSql.java @@ -0,0 +1,10 @@ +package doma.example.dao; + +import doma.example.entity.*; +import org.seasar.doma.*; + +@Dao +interface RenameDaoMethodWithoutSql { + @Insert + int notExistSql(Employee employee); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/RenameDaoMethodWithoutSqlAfter.java b/src/test/testData/src/main/java/doma/example/dao/RenameDaoMethodWithoutSqlAfter.java new file mode 100644 index 00000000..6a8d3248 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/RenameDaoMethodWithoutSqlAfter.java @@ -0,0 +1,10 @@ +package doma.example.dao; + +import doma.example.entity.*; +import org.seasar.doma.*; + +@Dao +interface RenameDaoMethodWithoutSql { + @Insert + int notExistSqlAfter(Employee employee); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/ScriptTestDao.java b/src/test/testData/src/main/java/doma/example/dao/ScriptTestDao.java new file mode 100644 index 00000000..341ef08f --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/ScriptTestDao.java @@ -0,0 +1,21 @@ +package doma.example.dao; + +import doma.example.entity.*; +import org.seasar.doma.*; + +@Dao +interface ScriptTestDao { + + @Script + void existsSQLFile(); + + @Script + void nonExistSQLFileError1(); + + @Script + void nonExistSQLFileError2(); + + @Sql("create table employee (id int, name varchar(10))") + @Script + void nonExistSQLFileAndTemplateIncluded(); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/SelectTestDao.java b/src/test/testData/src/main/java/doma/example/dao/SelectTestDao.java new file mode 100644 index 00000000..3d399ea0 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/SelectTestDao.java @@ -0,0 +1,21 @@ +package doma.example.dao; + +import java.util.List; +import doma.example.entity.*; +import org.seasar.doma.*; + + +@Dao +interface SelectTestDao { + + @Select + List existsSQLFile(String name); + + @Select + List nonExistSQLFileError(String name); + + @Select + @Sql("select * from employee where name = /* name */'test'") + Employee nonExistSQLFile(String name); + +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/SqlCompleteTestDao.java b/src/test/testData/src/main/java/doma/example/dao/SqlCompleteTestDao.java new file mode 100644 index 00000000..286f6bb6 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/SqlCompleteTestDao.java @@ -0,0 +1,60 @@ +package doma.example.dao; + +import doma.example.entity.*: +import org.seasar.doma.*; +import org.seasar.doma.jdbc.Config; +import org.seasar.doma.jdbc.PreparedSql; +import org.seasar.doma.jdbc.SelectOptions; + +import java.util.List; +import java.util.function.BiFunction; + +@Dao +interface SqlCompleteTestDao { + + @Select + Employee completeDaoArgument(Employee employee, String name); + + @Select + Employee completeInstancePropertyFromDaoArgumentClass(Employee employee, String name); + + @Insert(sqlFile = true) + int completeJavaPackageClass(Employee employee); + + @Update(sqlFile = true) + int completeDirective(Employee employee); + + @Select + Project completeStaticPropertyFromStaticPropertyCall(ProjectDetail detail); + + @Select + Project completePropertyAfterStaticPropertyCall(); + + @Select + Project completeBuiltinFunction(ProjectDetail detail); + + @Update(sqlFile = true) + int completeDirectiveInsideIf(Employee employee,Project project); + + @Update(sqlFile = true) + int completeDirectiveInsideElseIf(Employee employee,Project project); + + @Update(sqlFile = true) + int completeDirectiveInsideFor(Employee employee,Project project); + + @Update(sqlFile = true) + int completeDirectiveFieldInsideIf(Employee employee,Project project); + + @Update(sqlFile = true) + int completeDirectiveFieldInsideElseIf(Employee employee,Project project); + + @Update(sqlFile = true) + int completeDirectiveFieldInsideFor(Employee employee,Project project); + + @Select + EmployeeSummary completeComparisonOperator(EmployeeSummary summary); + + @Insert(sqlFile = true) + int completeConcatenationOperator(Employee employee,Integer point); + +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/SqlProcessorTestDao.java b/src/test/testData/src/main/java/doma/example/dao/SqlProcessorTestDao.java new file mode 100644 index 00000000..f4f53d2e --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/SqlProcessorTestDao.java @@ -0,0 +1,23 @@ +package doma.example.dao; + +import doma.example.entity.*; +import org.seasar.doma.*; + +import org.seasar.doma.jdbc.PreparedSql; + +import org.seasar.doma.jdbc.Config; +import java.util.function.BiFunction; + +@Dao +interface SqlProcessorTestDao { + + @SqlProcessor + R existsSQLFile(Integer id, BiFunction handler); + + @SqlProcessor + R nonExistSQLFileError(Integer id, BiFunction handler); + + @Sql("select * from employee where id = /* id */0") + @SqlProcessor + R nonExistSQLFile(Integer id, BiFunction handler); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/UpdateTestDao.java b/src/test/testData/src/main/java/doma/example/dao/UpdateTestDao.java new file mode 100644 index 00000000..ffd1386b --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/UpdateTestDao.java @@ -0,0 +1,21 @@ +package doma.example.dao; + +import doma.example.entity.*; +import org.seasar.doma.*; + +@Dao +interface UpdateTestDao { + + @Update + int nonExistSQLFile(Employee employee); + + @Update(sqlFile = true) + @Sql("update employee set id = /* employee.employeeId */0, name = /* employee.userName */'name'") + int nonExistSQLFileAndTemplateIncluded(Employee employee); + + @Update(sqlFile = true) + int nonExistSQLFileError(Employee employee); + + @Update(sqlFile = true) + int existsSQLFile(Employee employee); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/BatchDeleteGutterTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/BatchDeleteGutterTestDao.java new file mode 100644 index 00000000..98c22b84 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/BatchDeleteGutterTestDao.java @@ -0,0 +1,16 @@ +package doma.example.dao.gutteraction; + +import java.util.List; +import doma.example.entity.Employee; +import org.seasar.doma.BatchDelete; +import org.seasar.doma.Dao; + +@Dao +public interface BatchDeleteGutterTestDao { + + @BatchDelete(sqlFile = true) + int[] existsSQLFile1(List employees); + + @BatchDelete(sqlFile = true) + int[] existsSQLFile2(List employees); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/BatchInsertGutterTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/BatchInsertGutterTestDao.java new file mode 100644 index 00000000..6bbaf840 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/BatchInsertGutterTestDao.java @@ -0,0 +1,26 @@ +package doma.example.dao.gutteraction; + +import java.util.List; +import doma.example.entity.Employee; +import org.seasar.doma.BatchInsert; +import org.seasar.doma.Dao; + +@Dao +interface BatchInsertGutterTestDao { + + + @BatchInsert + int[] nonExistSQLFile(List employees); + + + @BatchInsert(sqlFile = true) + int[] existsSQLFile1(List employees); + + + @BatchInsert(sqlFile = true) + int[] existsSQLFile2(List employees); + + + @BatchInsert(sqlFile = true) + int[] existsSQLFile3(List employees); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/BatchUpdateGutterTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/BatchUpdateGutterTestDao.java new file mode 100644 index 00000000..71d15a6e --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/BatchUpdateGutterTestDao.java @@ -0,0 +1,25 @@ +package doma.example.dao.gutteraction; + +import java.util.List; +import doma.example.entity.Employee; +import org.seasar.doma.BatchUpdate; +import org.seasar.doma.Dao; + +@Dao +interface BatchUpdateGutterTestDao { + + @BatchUpdate + int[] nonExistSQLFile(List employee); + + @BatchUpdate(sqlFile = true) + int[] existsSQLFile1(List employees); + + @BatchUpdate(sqlFile = true) + int[] nonExistSQLFileError(List employees); + + @BatchUpdate(sqlFile = true) + int[] existsSQLFile2(List employees); + + @BatchUpdate(sqlFile = true) + int[] existsSQLFile3(List employees); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/DeleteGutterTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/DeleteGutterTestDao.java new file mode 100644 index 00000000..686d7b34 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/DeleteGutterTestDao.java @@ -0,0 +1,18 @@ +package doma.example.dao.gutteraction; + +import doma.example.entity.Employee; +import org.seasar.doma.Dao; +import org.seasar.doma.Delete; + +@Dao +interface DeleteGutterTestDao { + + @Delete + int nonExistSQLFile(Employee employee); + + @Delete(sqlFile = true) + int nonExistSQLFileError(Employee employee); + + @Delete(sqlFile = true) + int existsSQLFile1(Employee employee); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/InsertGutterTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/InsertGutterTestDao.java new file mode 100644 index 00000000..6c17c778 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/InsertGutterTestDao.java @@ -0,0 +1,26 @@ +package doma.example.dao.gutteraction; + +import doma.example.entity.Employee; +import org.seasar.doma.Dao; +import org.seasar.doma.Insert; +import org.seasar.doma.Sql; + +@Dao +interface InsertGutterTestDao { + + @Insert + int nonExistSQLFile(Employee employee); + + @Insert + @Sql("insert into employee (id, name) values (/* employee.detail.detail.detaiDemolname */0, /* employee.name */'')") + int nonExistSQLFileAndTemplateIncluded(Employee employee); + + @Insert(sqlFile = true) + int nonExistSQLFileError(Employee employee, String orderBy); + + @Insert(sqlFile = true) + int existsSQLFile1(Employee employee, String orderBy); + + @Insert(sqlFile = true) + int existsSQLFile2(Employee employee, String orderBy); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/JumpActionTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/JumpActionTestDao.java new file mode 100644 index 00000000..ac46ba92 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/JumpActionTestDao.java @@ -0,0 +1,28 @@ +package doma.example.dao.gutteraction; + +import java.util.List; +import doma.example.entity.*; +import org.seasar.doma.*; + +@Dao +interface JumpActionTestDao { + + @Select + List jumpToDaoFile(String name); + + @Select + List jumpToDaoMethodArgumentDefinition(Project project); + + @Select + List jumpToClassFieldDefinition(Employee employee); + + @Insert(sqlFile = true) + int jumpsToClassMethodDefinition(Employee employee); + + @Insert(sqlFile = true) + int jumpToStaticFieldDefinition(ProjectDetail detail); + + @Insert(sqlFile = true) + int jumpToStaticMethodDefinition(ProjectDetail detail); + +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/ScriptGutterTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/ScriptGutterTestDao.java new file mode 100644 index 00000000..540a5b52 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/ScriptGutterTestDao.java @@ -0,0 +1,25 @@ +package doma.example.dao.gutteraction; + +import org.seasar.doma.Dao; +import org.seasar.doma.Script; +import org.seasar.doma.Sql; + +@Dao +interface ScriptGutterTestDao { + + @Script + void existsSQLFile1(); + + @Script + void nonExistSQLFileError(); + + @Script + void nonExistSQLFile(); + + @Sql("create table employee (id int, name varchar(10))") + @Script + void nonExistSQLFileAndTemplateIncluded(); + + @Script + void existsSQLFile2(); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/SelectGutterTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/SelectGutterTestDao.java new file mode 100644 index 00000000..464d19a8 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/SelectGutterTestDao.java @@ -0,0 +1,25 @@ +package doma.example.dao.gutteraction; + +import java.util.List; +import doma.example.entity.Employee; +import org.seasar.doma.Dao; +import org.seasar.doma.Select; +import org.seasar.doma.Sql; + +@Dao +interface SelectGutterTestDao { + + @Select + List existsSQLFile1(String name); + + @Select + List existsSQLFile2(int id,Integer subId); + + @Select + List nonExistSQLFileError(String name); + + @Select + @Sql("select * from employee where name = /* name */'test'") + Employee nonExistSQLFile(String name); + +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/SqlProcessorGutterTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/SqlProcessorGutterTestDao.java new file mode 100644 index 00000000..2a4b4a0b --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/SqlProcessorGutterTestDao.java @@ -0,0 +1,26 @@ +package doma.example.dao.gutteraction; + +import org.seasar.doma.Dao; +import org.seasar.doma.SqlProcessor; +import org.seasar.doma.Sql; +import org.seasar.doma.jdbc.PreparedSql; + +import org.seasar.doma.jdbc.Config; +import java.util.function.BiFunction; + +@Dao +interface SqlProcessorGutterTestDao { + + @SqlProcessor + R existsSQLFile1(Integer id, BiFunction handler); + + @SqlProcessor + R nonExistSQLFileError(Integer id, BiFunction handler); + + @Sql("select * from employee where id = /* id */0") + @SqlProcessor + R nonExistSQLFile(Integer id, BiFunction handler); + + @SqlProcessor + R existsSQLFile2(Integer id, BiFunction handler); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/UpdateGutterTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/UpdateGutterTestDao.java new file mode 100644 index 00000000..c80dffa0 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/UpdateGutterTestDao.java @@ -0,0 +1,24 @@ +package doma.example.dao.gutteraction; + +import doma.example.entity.Employee; +import org.seasar.doma.Dao; +import org.seasar.doma.Update; + +@Dao +interface UpdateGutterTestDao { + + @Update + int nonExistSQLFile(Employee employee); + + @Update(sqlFile = true) + int existsSQLFile1(Employee employee); + + @Update + int nonExistSQLFile(Employee employee); + + @Update(sqlFile = true) + int nonExistSQLFileError(Employee employee); + + @Update(sqlFile = true) + int existsSQLFile2(Employee employee); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/BatchDeleteInvalidCaretTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/BatchDeleteInvalidCaretTestDao.java new file mode 100644 index 00000000..18801dfb --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/BatchDeleteInvalidCaretTestDao.java @@ -0,0 +1,14 @@ +package doma.example.dao.gutteraction; + +import java.util.List; +import doma.example.entity.Employee; +import org.seasar.doma.BatchDelete; +import org.seasar.doma.Dao; + +@Dao +public interface BatchDeleteInvalidCaretTestDao { + + @BatchDelete + int[] nonRequireSQLFile(List employees); + +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/BatchInsertInvalidCaretTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/BatchInsertInvalidCaretTestDao.java new file mode 100644 index 00000000..5368283f --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/BatchInsertInvalidCaretTestDao.java @@ -0,0 +1,15 @@ +package doma.example.dao.gutteraction; + +import java.util.List; +import doma.example.entity.Employee; +import org.seasar.doma.BatchInsert; +import org.seasar.doma.Dao; + +@Dao +interface BatchInsertInvalidCaretTestDao { + + + @BatchInsert(sqlFile = true) + int[] NotSQLExistError(List employees); + +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/BatchUpdateInvalidCaretTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/BatchUpdateInvalidCaretTestDao.java new file mode 100644 index 00000000..043aaeae --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/BatchUpdateInvalidCaretTestDao.java @@ -0,0 +1,22 @@ +package doma.example.dao.gutteraction; + +import java.util.List; +import doma.example.entity.Employee; +import org.seasar.doma.BatchUpdate; +import org.seasar.doma.Dao; + +@Dao +interface BatchUpdateInvalidCaretTestDao { + + + @BatchUpdate(sqlFile = true) + @Sql("update employee set name = /* employees.name */'test' where id = /* employees.id */1") + int[] NoSqlFileWithTemplate(List employees); + + @BatchUpdate(sqlFile = true) + int[] existsSQLFile1(List employees); + + @BatchUpdate(sqlFile = true) + int[] existsSQLFile2(List employees); + +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/DeleteInvalidCaretTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/DeleteInvalidCaretTestDao.java new file mode 100644 index 00000000..fb2b453a --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/DeleteInvalidCaretTestDao.java @@ -0,0 +1,19 @@ +package doma.example.dao.gutteraction; + +import doma.example.entity.Employee; +import org.seasar.doma.Dao; +import org.seasar.doma.Delete; + +@Dao +interface DeleteInvalidCaretTestDao { + + + @Delete + int nonRequireSQLFile(Employee employee); + + @Delete(sqlFile = true) + int nonExistSQLFileError(Employee employee); + + @Delete(sqlFile = true) + int existsSQLFile1(Employee employee); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/InsertInvalidCaretTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/InsertInvalidCaretTestDao.java new file mode 100644 index 00000000..ba6e38ee --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/InsertInvalidCaretTestDao.java @@ -0,0 +1,20 @@ +package doma.example.dao.gutteraction; + +import doma.example.entity.Employee; +import org.seasar.doma.Dao; +import org.seasar.doma.Insert; +import org.seasar.doma.Sql; + +@Dao +interface InsertInvalidCaretTestDao { + + @Insert + @Sql("insert into employee (id, name) values (/* employee.detail.detail.detaiDemolname */0, /* employee.name */'')") + int NoSqlFileWithTemplate(Employee employee); + + @Insert(sqlFile = true) + int existsSQLFile1(Employee employee, String orderBy); + + @Insert(sqlFile = true) + int NoSqlFile(Employee employee, String orderBy); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/InvalidCaretTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/InvalidCaretTestDao.java new file mode 100644 index 00000000..8c244a2d --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/InvalidCaretTestDao.java @@ -0,0 +1,15 @@ +package doma.example.dao.gutteraction; + +import java.util.List; +import doma.example.entity.Employee; +import org.seasar.doma.Dao; +import org.seasar.doma.Select; +import org.seasar.doma.Sql; + +@Dao +interface InvalidCaretTestDao { + + @Select + Employee nonExistSQLFile(String name); + +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/ScriptInvalidCaretTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/ScriptInvalidCaretTestDao.java new file mode 100644 index 00000000..e4aab56c --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/ScriptInvalidCaretTestDao.java @@ -0,0 +1,25 @@ +package doma.example.dao.gutteraction; + +import org.seasar.doma.Dao; +import org.seasar.doma.Script; +import org.seasar.doma.Sql; + +@Dao +interface ScriptInvalidCaretTestDao { + + @Script + void existsSQLFile1(); + + @Script + void nonExistSQLFileError(); + + @Script + void nonExistSQLFile(); + + @Sql("create table employee (id int, name varchar(10))") + @Script + void NoSqlFileWithTemplate(); + + @Script + void existsSQLFile2(); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/SelectInvalidCaretTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/SelectInvalidCaretTestDao.java new file mode 100644 index 00000000..a9d75ab2 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/SelectInvalidCaretTestDao.java @@ -0,0 +1,15 @@ +package doma.example.dao.gutteraction; + +import java.util.List; +import doma.example.entity.Employee; +import org.seasar.doma.Dao; +import org.seasar.doma.Select; +import org.seasar.doma.Sql; + +@Dao +interface SelectInvalidCaretTestDao { + + @Select + Employee NotSQLExistError(String name); + +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/SqlProcessorInvalidCaretTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/SqlProcessorInvalidCaretTestDao.java new file mode 100644 index 00000000..0c78f514 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/SqlProcessorInvalidCaretTestDao.java @@ -0,0 +1,25 @@ +package doma.example.dao.gutteraction; + +import org.seasar.doma.Dao; +import org.seasar.doma.SqlProcessor; +import org.seasar.doma.Sql; +import org.seasar.doma.jdbc.PreparedSql; + +import org.seasar.doma.jdbc.Config; +import java.util.function.BiFunction; + +@Dao +interface SqlProcessorInvalidCaretTestDao { + + @SqlProcessor + R existsSQLFile1(Integer id, BiFunction handler); + + + + @Sql("select * from employee where id = /* id */0") + @SqlProcessor + R nonExistSQLFile(Integer id, BiFunction handler); + + @SqlProcessor + R NotSQLExistError(Integer id, BiFunction handler); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/UpdateInvalidCaretTestDao.java b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/UpdateInvalidCaretTestDao.java new file mode 100644 index 00000000..1d8c2992 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/gutteraction/notdisplayed/UpdateInvalidCaretTestDao.java @@ -0,0 +1,21 @@ +package doma.example.dao.gutteraction; + +import doma.example.entity.*; +import org.seasar.doma.*; + +@Dao +interface UpdateInvalidCaretTestDao { + + @Update(sqlFile = true) + int existsSQLFile1(Employee employee); + + @Update(sqlFile = true) + @Sql("update employee set name = /* employee.name */'hoge' where id = /* employee.id */1") + int nonRequireSQLFile(Employee employee); + + @Update(sqlFile = true) + int nonExistSQLFileError(Employee employee); + + @Update(sqlFile = true) + int existsSQLFile2(Employee employee); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/quickfix/BatchDeleteQuickFixTestDao.java b/src/test/testData/src/main/java/doma/example/dao/quickfix/BatchDeleteQuickFixTestDao.java new file mode 100644 index 00000000..0734d862 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/quickfix/BatchDeleteQuickFixTestDao.java @@ -0,0 +1,24 @@ +package doma.example.dao.quickfix; + +import java.util.List; +import doma.example.entity.Employee; +import org.seasar.doma.BatchDelete; +import org.seasar.doma.Dao; + +@Dao +public interface BatchDeleteQuickFixTestDao { + + + + @BatchDelete + int[] nonExistSQLFile(List employees); + + + @BatchDelete(sqlFile = true) + int[] generateSQLFile(List employees); + + + @BatchDelete(sqlFile = true) + int[] existsSQLFile(List employees); + +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/quickfix/BatchInsertQuickFixTestDao.java b/src/test/testData/src/main/java/doma/example/dao/quickfix/BatchInsertQuickFixTestDao.java new file mode 100644 index 00000000..13171940 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/quickfix/BatchInsertQuickFixTestDao.java @@ -0,0 +1,22 @@ +package doma.example.dao.quickfix; + +import java.util.List; +import doma.example.entity.Employee; +import org.seasar.doma.BatchInsert; +import org.seasar.doma.Dao; + +@Dao +interface BatchInsertQuickFixTestDao { + + + @BatchInsert + int[] nonExistSQLFile(List employees); + + + @BatchInsert(sqlFile = true) + int[] generateSQLFile(List employees); + + + @BatchInsert(sqlFile = true) + int[] existsSQLFile(List employees); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/quickfix/BatchUpdateQuickFixTestDao.java b/src/test/testData/src/main/java/doma/example/dao/quickfix/BatchUpdateQuickFixTestDao.java new file mode 100644 index 00000000..6dd01b66 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/quickfix/BatchUpdateQuickFixTestDao.java @@ -0,0 +1,22 @@ +package doma.example.dao.quickfix; + +import java.util.List; +import doma.example.entity.Employee; +import org.seasar.doma.BatchUpdate; +import org.seasar.doma.Dao; + +@Dao +interface BatchUpdateQuickFixTestDao { + + + @BatchUpdate + int[] nonExistSQLFile(List employee); + + + @BatchUpdate(sqlFile = true) + int[] generateSQLFile(List employees); + + + @BatchUpdate(sqlFile = true) + int[] existsSQLFile(List employees); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/quickfix/DeleteQuickFixTestDao.java b/src/test/testData/src/main/java/doma/example/dao/quickfix/DeleteQuickFixTestDao.java new file mode 100644 index 00000000..078db446 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/quickfix/DeleteQuickFixTestDao.java @@ -0,0 +1,22 @@ +package doma.example.dao.quickfix; + +import doma.example.entity.Employee; +import org.seasar.doma.Dao; +import org.seasar.doma.Delete; + +@Dao +interface DeleteQuickFixTestDao { + + + + @Delete + int nonExistSQLFile(Employee employee); + + + @Delete(sqlFile = true) + int generateSQLFile(Employee employee); + + + @Delete(sqlFile = true) + int existsSQLFile(Employee employee); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/quickfix/InsertQuickFixTestDao.java b/src/test/testData/src/main/java/doma/example/dao/quickfix/InsertQuickFixTestDao.java new file mode 100644 index 00000000..88c376c5 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/quickfix/InsertQuickFixTestDao.java @@ -0,0 +1,21 @@ +package doma.example.dao.quickfix; + +import doma.example.entity.*; +import org.seasar.doma.*; + +@Dao +interface InsertQuickFixTestDao { + + @Insert + int nonExistSQLFile(Employee employee); + + @Insert + @Sql("insert into employee (id, name) values (/* employee.detail.detail.detaiDemolname */0, /* employee.name */'')") + int nonExistSQLFileAndTemplateIncluded(Employee employee); + + @Insert(sqlFile = true) + int generateSQLFile(Employee employee, String orderBy); + + @Insert(sqlFile = true) + int existsSQLFile(Employee employee, String orderBy); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/quickfix/ScriptQuickFixTestDao.java b/src/test/testData/src/main/java/doma/example/dao/quickfix/ScriptQuickFixTestDao.java new file mode 100644 index 00000000..1ba24b96 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/quickfix/ScriptQuickFixTestDao.java @@ -0,0 +1,22 @@ +package doma.example.dao.quickfix; + +import org.seasar.doma.Dao; +import org.seasar.doma.Script; +import org.seasar.doma.Sql; + +@Dao +interface ScriptQuickFixTestDao { + + @Script + void existsSQLFile(); + + @Script + void generateSQLFile(); + + @Script + void nonExistSQLFileError(); + + @Sql("create table employee (id int, name varchar(10))") + @Script + void nonExistSQLFileAndTemplateIncluded(); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/quickfix/SelectQuickFixTestDao.java b/src/test/testData/src/main/java/doma/example/dao/quickfix/SelectQuickFixTestDao.java new file mode 100644 index 00000000..45533bc6 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/quickfix/SelectQuickFixTestDao.java @@ -0,0 +1,22 @@ +package doma.example.dao.quickfix; + +import java.util.List; +import doma.example.entity.Employee; +import org.seasar.doma.Dao; +import org.seasar.doma.Select; +import org.seasar.doma.Sql; + +@Dao +interface SelectQuickFixTestDao { + + @Select + List existsSQLFile(String name); + + @Select + List generateSQLFile(String name); + + @Select + @Sql("select * from employee where name = /* name */'test'") + Employee nonExistSQLFile(String name); + +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/quickfix/SqlProcessorQuickFixTestDao.java b/src/test/testData/src/main/java/doma/example/dao/quickfix/SqlProcessorQuickFixTestDao.java new file mode 100644 index 00000000..83315757 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/quickfix/SqlProcessorQuickFixTestDao.java @@ -0,0 +1,23 @@ +package doma.example.dao.quickfix; + +import org.seasar.doma.Dao; +import org.seasar.doma.SqlProcessor; +import org.seasar.doma.Sql; +import org.seasar.doma.jdbc.PreparedSql; + +import org.seasar.doma.jdbc.Config; +import java.util.function.BiFunction; + +@Dao +interface SqlProcessorQuickFixTestDao { + + @SqlProcessor + R existsSQLFile(Integer id, BiFunction handler); + + @SqlProcessor + R generateSQLFile(Integer id, BiFunction handler); + + @Sql("select * from employee where id = /* id */0") + @SqlProcessor + R nonExistSQLFile(Integer id, BiFunction handler); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/dao/quickfix/UpdateQuickFixTestDao.java b/src/test/testData/src/main/java/doma/example/dao/quickfix/UpdateQuickFixTestDao.java new file mode 100644 index 00000000..10562786 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/dao/quickfix/UpdateQuickFixTestDao.java @@ -0,0 +1,18 @@ +package doma.example.dao.quickfix; + +import doma.example.entity.Employee; +import org.seasar.doma.Dao; +import org.seasar.doma.Update; + +@Dao +interface UpdateQuickFixTestDao { + + @Update + int nonExistSQLFile(Employee employee); + + @Update(sqlFile = true) + int generateSQLFile(Employee employee); + + @Update(sqlFile = true) + int existsSQLFile(Employee employee); +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/entity/Employee.java b/src/test/testData/src/main/java/doma/example/entity/Employee.java new file mode 100644 index 00000000..0cf631a8 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/entity/Employee.java @@ -0,0 +1,30 @@ +package doma.example.entity; + +import java.time.LocalDate; +import org.seasar.doma.Entity; +import org.seasar.doma.Id; +import doma.example.entity.*; + +@Entity +public class Employee extends User { + + @Id + public Integer employeeId; + public String employeeName; + private String department; + private String rank; + public List projects; + + public Integer managerId; + + // accessible instance methods + public Project getFirstProject() { + return projects.get(0); + } + + // Inaccessible instance methods + private String getEmployeeRank() { + return rank; + } + +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/entity/EmployeeSummary.java b/src/test/testData/src/main/java/doma/example/entity/EmployeeSummary.java new file mode 100644 index 00000000..0cbaf9ef --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/entity/EmployeeSummary.java @@ -0,0 +1,17 @@ +package doma.example.entity; + +import org.seasar.doma.Id; + +//Inadashi bae member s tanse horse hand ds +public class EmployeeSummary { + + @Id + private String employeeId; + public Long userId; + public String userName; + private String email; + private String departmentId; + private int numberOfProjects; + + public Employee employee; +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/entity/Project.java b/src/test/testData/src/main/java/doma/example/entity/Project.java new file mode 100644 index 00000000..62dfb54c --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/entity/Project.java @@ -0,0 +1,38 @@ +package doma.example.entity; + +import java.time.LocalDate; +import org.seasar.doma.*; +import org.seasar.doma.Id; +import java.util.ArrayList; +import java.util.List; + +@Entity +public class Project { + + @Id + private Integer projectId; + private String projectName; + private static String status; + private Integer rank; + + // Accessible static fields + public static Integr projectNumber; + private static String projectCategory; + + public static Interer cost; + + public Employee getFirstEmployee() { + return employees.get(0); + } + + // Accessible Static methods + public static String getTermNumber() { + return projectNumber.toString()+"_term"; + } + + // Static methods that are not accessible + private static String getCategoryName() { + return projectCategory+"_term"; + } + +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/entity/ProjectDetail.java b/src/test/testData/src/main/java/doma/example/entity/ProjectDetail.java new file mode 100644 index 00000000..4547ab89 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/entity/ProjectDetail.java @@ -0,0 +1,42 @@ +package doma.example.entity; + +import doma.example.entity.*; +import java.time.LocalDate; +import org.seasar.doma.Entity; +import org.seasar.doma.Id; +import java.util.ArrayList; +import java.util.List; + +@Entity +public class ProjectDetail { + + @Id + private Integer projectDetailId; + private Integer projectId; + private static List members = new ArrayList<>(); + + // Accessible static fields + public static Integr projectNumber; + private static String projectCategory; + + private static Employee manager = new Employee(); + + public Employee getFirstEmployee() { + return employees.get(0); + } + + // Accessible Static methods + public static String getTermNumber() { + return projectNumber.toString()+"_term"; + } + + // Static methods that are not accessible + private static String getCategoryName() { + return projectCategory+"_term"; + } + + public void addTermNumber(){ + projectNumber++; + } + +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/entity/User.java b/src/test/testData/src/main/java/doma/example/entity/User.java new file mode 100644 index 00000000..dfff14cf --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/entity/User.java @@ -0,0 +1,34 @@ +package doma.example.entity; + +import java.time.LocalDate; +import org.seasar.doma.Entity; +import org.seasar.doma.Id; + +// Parent class that is not Entity +public class User { + + @Id + public Integer userId; + private String userName; + private String email; + + public User() { + } + + public User(Long userId, String userName, String email) { + this.userId = userId; + this.userName = userName; + this.email = email; + } + + // accessible parent public method + public String getUserNameFormat() { + return "User:" + userName; + } + + // Inaccessible parent private method + private String getEmail(){ + return email; + } + +} \ No newline at end of file diff --git a/src/test/testData/src/main/java/doma/example/entity/UserSummary.java b/src/test/testData/src/main/java/doma/example/entity/UserSummary.java new file mode 100644 index 00000000..415c26a7 --- /dev/null +++ b/src/test/testData/src/main/java/doma/example/entity/UserSummary.java @@ -0,0 +1,34 @@ +package doma.example.entity; + +@Entity +public class UserSummary { + private Long userId; + private String userName; + private String email; + private Integer numberOfProjects; + + public UserSummary() { + } + + public UserSummary(Long userId, String userName, String email, int numberOfProjects) { + this.userId = userId; + this.userName = userName; + this.email = email; + this.numberOfProjects = numberOfProjects; + } + + // Inaccessible instance method + private String getUserName() { + return userName; + } + + @Override + public String toString() { + return "UserSummary{" + + "userId=" + userId + + ", userName='" + userName + '\'' + + ", email='" + email + '\'' + + ", numberOfProjects=" + numberOfProjects + + '}'; + } +} \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/DaoMethodVariableInspectionTestDao/biFunctionDoesNotCauseError.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/DaoMethodVariableInspectionTestDao/biFunctionDoesNotCauseError.sql new file mode 100644 index 00000000..fe48dc6f --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/DaoMethodVariableInspectionTestDao/biFunctionDoesNotCauseError.sql @@ -0,0 +1,9 @@ +-- Using Dao arguments other than BiFunction as bind variables +select + e.employee_id + , e.employee_name + , e.employee_number + from employee e + inner join project p + on p.project_id = e.project_id + where p.project_id = /* id */1 diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/DaoMethodVariableInspectionTestDao/selectOptionDoesNotCauseError.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/DaoMethodVariableInspectionTestDao/selectOptionDoesNotCauseError.sql new file mode 100644 index 00000000..83b01e86 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/DaoMethodVariableInspectionTestDao/selectOptionDoesNotCauseError.sql @@ -0,0 +1,8 @@ +-- Using Dao arguments other than SelectOption as bind variables +select + p.project_id + , p.project_name + , p.project_number +from project p +where p.project_name = /* searchName */'name' + and num = 1 + 3 \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/EmployeeSummaryDao/accessStaticProperty.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/EmployeeSummaryDao/accessStaticProperty.sql new file mode 100644 index 00000000..f4bf62b7 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/EmployeeSummaryDao/accessStaticProperty.sql @@ -0,0 +1,39 @@ +-- Test using Static property as bind variable +update project p +set + -- Existing field access + p.project_name = /* project.projectName */'name' + -- non-existent field access + , p.project_description = /* project.projectDescription */'description' +from + project_term pt + inner join project_detail pd on p.project_id = pd.project_id +where + -- instance field access + p.project_id = /* project.projectId */0 + and p.project_id = pt.project_id + and pt.start_date >= /* referenceDate */'2099-12-31' + -- public static field access + and p.project_number <= /* project.projectNumber */0 + -- private static field access + and p.project_category = /* project.projectCategory */'category' + -- public static method access + and pt.project_term_number = /* project.getTermNumber() */'term' + -- private static method access + and pt.project_term_category_name = /* project.getCategoryName() */'termCategory' + -- static field call + and p.project_status = /* @doma.example.entity.Project@status */'TODO' + AND ( + -- static method call + /*%for member : @doma.example.entity.ProjectDetail@members*/ + /*%if member.hasNext() */ + pd.member_id = /* member.employeeId */0 + /*# "OR"*/ + /*%end */ + /*%end */ + ) + -- Static field call that does not exist + /*%if @doma.example.entity.ProjectDetail@priority >= 3 */ + -- Static method call that does not exist + AND pd.limit_date = /* @doma.example.entity.ProjectDetail@getLimit() */0 + /*%end */ \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/EmployeeSummaryDao/batchAnnotationResolvesClassInList.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/EmployeeSummaryDao/batchAnnotationResolvesClassInList.sql new file mode 100644 index 00000000..f961ace9 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/EmployeeSummaryDao/batchAnnotationResolvesClassInList.sql @@ -0,0 +1,50 @@ +-- If there is a List-type Dao argument in a Batch-based annotation, the class type inside can be referenced. + INSERT INTO employee_project (employee_name, department, project) + ( + SELECT + e1.employee_name, + e1.department, + p1.project + FROM + employee e1 + JOIN user u1 ON e1.employee_id = u1.user_id + -- Access to parent private field + WHERE u1.user_name = /* employees.userName.toLowerCase() */'name' + -- Access to non-existent parent field + OR u1.user_name = /* employees.userFirstName.toLowerCase() */'name' + -- Public parent method + OR u1.user_name = /* employees.getUserNameFormat() */'name' + -- Private parent method + OR u1.email = /* employees.getEmail() */'email' + JOIN project p1 ON e1.employee_id = p1.employee_id + WHERE + -- Public entity method + p1.project_id = /* employees.getFirstProject().projectId */0 + -- Private entity method + AND p1.base_rank >= /* employees.getEmployeeRank() */0 + WHERE + -- Access to private field of entity class + e1.employee_id = /* employees.employeeId */0 + /*%for project : employees.projects */ + UNION ALL + SELECT + e2.employee_name, + e2.department, + -- Use of for item + /* project */'project' + FROM + employee e2 + JOIN user u2 ON e2.employee_id = u2.user_id + -- Access to parent public field + AND u2.user_id = /* employees.userId */0 + WHERE + -- Access to public field of entity class + e2.employee_id = /* employees.employeeId */0 + -- Access to private field of entity class + AND e2.department = /* employees.department */'department' + -- Access to non-existent field of entity class + OR AND e2.department = /* employees.subDepartment */'department' + -- Access to non-existent method of entity class + OR AND e2.department = /* employees.getFirstDepartment() */'department' + /*%end*/ + ) \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/EmployeeSummaryDao/bindVariableForEntityAndNonEntityParentClass.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/EmployeeSummaryDao/bindVariableForEntityAndNonEntityParentClass.sql new file mode 100644 index 00000000..6f87f8ee --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/EmployeeSummaryDao/bindVariableForEntityAndNonEntityParentClass.sql @@ -0,0 +1,49 @@ +INSERT INTO employee_project (employee_name, department, project) + ( + SELECT + e1.employee_name, + e1.department, + p1.project + FROM + employee e1 + JOIN user u1 ON e1.employee_id = u1.user_id + -- Access to parent private field + WHERE u1.user_name = /* employee.userName.toLowerCase() */'name' + -- Access to non-existent parent field + OR u1.user_name = /* employee.userFirstName.toLowerCase() */'name' + -- Public parent method + OR u1.user_name = /* employee.getUserNameFormat() */'name' + -- Private parent method + OR u1.email = /* employee.getEmail() */'email' + JOIN project p1 ON e1.employee_id = p1.employee_id + WHERE + -- Public entity method + p1.project_id = /* employee.getFirstProject().projectId */0 + -- Private entity method + AND p1.base_rank >= /* employee.getEmployeeRank() */0 + WHERE + -- Access to private field of entity class + e1.employee_id = /* employee.employeeId */0 + /*%for project : employee.projects */ + UNION ALL + SELECT + e2.employee_name, + e2.department, + -- Use of for item + /* project */'project' + FROM + employee e2 + JOIN user u2 ON e2.employee_id = u2.user_id + -- Access to parent public field + AND u2.user_id = /* employee.userId */0 + WHERE + -- Access to public field of entity class + e2.employee_id = /* employee.employeeId */0 + -- Access to private field of entity class + AND e2.department = /* employee.department */'department' + -- Access to non-existent field of entity class + OR AND e2.department = /* employee.subDepartment */'department' + -- Access to non-existent method of entity class + OR AND e2.department = /* employee.getFirstDepartment() */'department' + /*%end*/ + ) \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/EmployeeSummaryDao/bindVariableForNonEntityClass.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/EmployeeSummaryDao/bindVariableForNonEntityClass.sql new file mode 100644 index 00000000..eef4782f --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/EmployeeSummaryDao/bindVariableForNonEntityClass.sql @@ -0,0 +1,38 @@ +-- Test referencing instance fields and methods of a non-Entity class + SELECT + e.employee_id + , u.user_id + , u.user_name + , u.email + , e.department + , COUNT(pe.project_id) + -- Field reference using an operator + , /* employee.numberOfProjects * 10 */ AS projectPoint + FROM user u + JOIN employee e + ON u.user_id = e.user_id + LEFT JOIN project_employee pe + ON e.employee_id = pe.employee_id + WHERE + -- Reference to a public field + e.user_id = /* user.userId */0 + AND e.employee_id = /* employee.employeeId */0 + -- if statement using a private field + /*%if employee.departmentId.startsWith("200") */ + AND e.department_id = /* employee.departmentId */'dept' + /*%elseif employee.numberOfProjects >= 3 */ + AND pe.start_date <= CURRENT_DATE + AND pe.end_date >= CURRENT_DATE + /*%end*/ + -- Reference error for a non-existent field + /*%for child : employee.projectIds */ + AND pe.parent_project = /* child.projectId */0 + -- Reference error for a non-existent method + AND pe.member_id IN /* employee.getTopProject() */(0,1,2) + /*%end */ + GROUP BY + e.employee_id, + u.user_id, + u.user_name, + u.email, + e.department \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/EmployeeSummaryDao/resolveDaoArgumentOfListType.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/EmployeeSummaryDao/resolveDaoArgumentOfListType.sql new file mode 100644 index 00000000..267c7af8 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/EmployeeSummaryDao/resolveDaoArgumentOfListType.sql @@ -0,0 +1,23 @@ +-- Use List-type Dao argument directly as a List-type bind variable + select p.project_id + , pd.project_detail_id + , pd.project_number + , p.project_name + , e.employee_id + , e.employee_name + from project_detail pd + inner join project p + on p.project_id = pd.project_id + inner join employee e + on pd.employee_id = e.employee_id + -- Use as List-type + /*%if employees.size() > 0 */ + where + /*%for member : employees */ + p.employee_id = /* member.employee_id */0 + /*%end */ + -- Cannot be used as a class inside List-type + /*%elseif employees.rank > 3*/ + p.employee_id = /* employees.employeeId */0 + and p.base_rank = /* employees.rank */0 + /*%end */ \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeBuiltinFunction.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeBuiltinFunction.sql new file mode 100644 index 00000000..388cbcb4 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeBuiltinFunction.sql @@ -0,0 +1,18 @@ +-- completeBuiltinFunction +select + p.project_id + , p.statis + , p.project_name + , p.rank +from project p + inner join project_detail pd + on p.project_id = pd.project_id + where + p.project_id = /* detail.projectId */0 + /*%if @is */ + -- Code completion for static fields and methods + and pd.status = /* @doma.example.entity.ProjectDetail@status */'TODO' + /*%for member: detail.members */ + and pd.member_id = /* member.memberId */0 + /*%end */ + /*%end */ \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeComparisonOperator.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeComparisonOperator.sql new file mode 100644 index 00000000..850df458 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeComparisonOperator.sql @@ -0,0 +1,10 @@ +select + e.employee_id + , e.employee_name + , e.rank +from employee e + inner join project p on e.employee_id = p.employee_id +where e.employee_id = /* summary.employeeId */1 +/*%if @doma.example.entity.Project@rank == summary.employee.r */ + and e.project_cost >= /* @doma.example.entity.Project@cost */9999 +/*%end */ \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeConcatenationOperator.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeConcatenationOperator.sql new file mode 100644 index 00000000..86e13d9b --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeConcatenationOperator.sql @@ -0,0 +1,4 @@ +insert into employee (id, name, rank) values ( +/* employee.employeeId */1, +/* employee.employeeName */'name', +/* 1 + point + employee.r */1) \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDaoArgument.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDaoArgument.sql new file mode 100644 index 00000000..5edd4dbf --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDaoArgument.sql @@ -0,0 +1 @@ +select * from employee where id = /* */1 \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirective.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirective.sql new file mode 100644 index 00000000..6d66a3f4 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirective.sql @@ -0,0 +1,13 @@ +update employee e set + e.employee_name = /* employee.employeeName */'name' + , e.rank = /* employee.rank */1 +where + e.employee_id = /* employee.employeeId */1 + /*%if employee.department.startWith("200") */ + and e.department = /* employee.department */'department' + /*%e employee.department.startWith("100") */ + and e.sub_department = /* employee.department */'department' + and e.rank >= /* employee.rank */9999 + /*%else */ + and e.sub_department = /* employee.department */'department' + /*%end*/ \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveFieldInsideElseIf.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveFieldInsideElseIf.sql new file mode 100644 index 00000000..fa1d492c --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveFieldInsideElseIf.sql @@ -0,0 +1,13 @@ +update employee e set + e.employee_name = /* employee.employeeName */'name' + , e.rank = /* employee.rank */1 +where + e.employee_id = /* employee.employeeId */1 + /*%if employee.department.startWith("200") */ + and e.department = /* employee.department */'department' + /*%elseif employee.d */ + and e.sub_department = /* employee.department */'department' + and e.rank >= /* employee.rank */9999 + /*%else */ + and e.sub_department = /* employee.department */'department' + /*%end*/ \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveFieldInsideFor.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveFieldInsideFor.sql new file mode 100644 index 00000000..22a995d3 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveFieldInsideFor.sql @@ -0,0 +1,15 @@ +update employee e set + e.employee_name = /* employee.employeeName */'name' + , e.rank = /* employee.rank */1 +where + e.employee_id = /* employee.employeeId */1 + /*%for member : project. */ + /*%if member.department.startWith("200") */ + and e.department = /* member.department */'department' + /*%elseif member.department.startWith("100") */ + and e.sub_department = /* member.department */'department' + and e.rank >= /* member.rank */9999 + /*%else */ + and e.sub_department = /* member.department */'department' + /*%end*/ + /*%end*/ \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveFieldInsideIf.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveFieldInsideIf.sql new file mode 100644 index 00000000..48111f25 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveFieldInsideIf.sql @@ -0,0 +1,13 @@ +update employee e set + e.employee_name = /* employee.employeeName */'name' + , e.rank = /* employee.rank */1 +where + e.employee_id = /* employee.employeeId */1 + /*%if employee.department.start */ + and e.department = /* employee.department */'department' + /*%elseif employee.department.startWith("100") */ + and e.sub_department = /* employee.department */'department' + and e.rank >= /* employee.rank */9999 + /*%else */ + and e.sub_department = /* employee.department */'department' + /*%end*/ \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveInsideElseIf.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveInsideElseIf.sql new file mode 100644 index 00000000..1b9bcbf6 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveInsideElseIf.sql @@ -0,0 +1,13 @@ +update employee e set + e.employee_name = /* employee.employeeName */'name' + , e.rank = /* employee.rank */1 +where + e.employee_id = /* employee.employeeId */1 + /*%if employee.department.startWith("200") */ + and e.department = /* employee.department */'department' + /*%elseif */ + and e.sub_department = /* employee.department */'department' + and e.rank >= /* employee.rank */9999 + /*%else */ + and e.sub_department = /* employee.department */'department' + /*%end*/ \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveInsideFor.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveInsideFor.sql new file mode 100644 index 00000000..78aa9431 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveInsideFor.sql @@ -0,0 +1,15 @@ +update employee e set + e.employee_name = /* employee.employeeName */'name' + , e.rank = /* employee.rank */1 +where + e.employee_id = /* employee.employeeId */1 + /*%for member : p */ + /*%if member.department.startWith("200") */ + and e.department = /* member.department */'department' + /*%elseif member.department.startWith("100") */ + and e.sub_department = /* member.department */'department' + and e.rank >= /* member.rank */9999 + /*%else */ + and e.sub_department = /* member.department */'department' + /*%end*/ + /*%end*/ \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveInsideIf.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveInsideIf.sql new file mode 100644 index 00000000..807f4b32 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeDirectiveInsideIf.sql @@ -0,0 +1,13 @@ +update employee e set + e.employee_name = /* employee.employeeName */'name' + , e.rank = /* employee.rank */1 +where + e.employee_id = /* employee.employeeId */1 + /*%if e */ + and e.department = /* employee.department */'department' + /*%elseif employee.department.startWith("100") */ + and e.sub_department = /* employee.department */'department' + and e.rank >= /* employee.rank */9999 + /*%else */ + and e.sub_department = /* employee.department */'department' + /*%end*/ \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeInstancePropertyFromDaoArgumentClass.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeInstancePropertyFromDaoArgumentClass.sql new file mode 100644 index 00000000..887b3716 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeInstancePropertyFromDaoArgumentClass.sql @@ -0,0 +1 @@ +select * from employee where id = /* employee. */1 \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeJavaPackageClass.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeJavaPackageClass.sql new file mode 100644 index 00000000..537706d1 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeJavaPackageClass.sql @@ -0,0 +1 @@ +insert into employee (id, name) values (/* employee.employeeId */0, /* employee.employeeName. */'test') \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completePropertyAfterStaticPropertyCall.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completePropertyAfterStaticPropertyCall.sql new file mode 100644 index 00000000..22c4aa5c --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completePropertyAfterStaticPropertyCall.sql @@ -0,0 +1,11 @@ +select + p.project_id + , p.statis + , p.project_name + , p.rank +from project p + inner join project_detail pd + on p.project_id = pd.project_id + where + -- Code completion for static fields and methods + and pd.manager_id = /* @doma.example.entity.ProjectDetail@manager.m */'TODO' \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeStaticPropertyFromStaticPropertyCall.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeStaticPropertyFromStaticPropertyCall.sql new file mode 100644 index 00000000..882d2219 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/SqlCompleteTestDao/completeStaticPropertyFromStaticPropertyCall.sql @@ -0,0 +1,17 @@ +select + p.project_id + , p.statis + , p.project_name + , p.rank +from project p + inner join project_detail pd + on p.project_id = pd.project_id + where + p.project_id = /* detail.projectId */0 + /*%if @isNotEmpty(detail.members) */ + -- Code completion for static fields and methods + and pd.type = /* @doma.example.entity.ProjectDetail@ */'TODO' + /*%for member: detail.members */ + and pd.member_id = /* member.memberId */0 + /*%end */ + /*%end */ \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToClassFieldDefinition.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToClassFieldDefinition.sql new file mode 100644 index 00000000..bb60de77 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToClassFieldDefinition.sql @@ -0,0 +1,6 @@ +insert into project_manager ( + project_manager_id + , manager_name) +values (/* employee.getFirstProject().projectId */1 + , /* employee.employeeName */'name' + ) \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToDaoFile.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToDaoFile.sql new file mode 100644 index 00000000..290263ea --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToDaoFile.sql @@ -0,0 +1,2 @@ +-- Gutter, jump to SelectDao.java with action call +select * from emp where id = /* id */1 \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToDaoMethodArgumentDefinition.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToDaoMethodArgumentDefinition.sql new file mode 100644 index 00000000..e917f34f --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToDaoMethodArgumentDefinition.sql @@ -0,0 +1,4 @@ +select * from employee e + inner join project p + on p.employee_id = e.employee_id + where p.employee_id IN /* project.members */(0,1,2) \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToStaticFieldDefinition.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToStaticFieldDefinition.sql new file mode 100644 index 00000000..bfeb219c --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToStaticFieldDefinition.sql @@ -0,0 +1,13 @@ +insert into project_detail ( + project_detail_id + , project_id + , project_number + , project_term_number + , projec_category) +values ( + /* detail.projectDetailId */1, + , /* detail.projectId */1 + , /* detail.projectNumber */1 + , /* @doma.example.entity.ProjectDetail@getTermNumber() */'term' + , /* @doma.example.entity.ProjectDetail@projectCategory */'category' + ) \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToStaticMethodDefinition.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToStaticMethodDefinition.sql new file mode 100644 index 00000000..80537426 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpToStaticMethodDefinition.sql @@ -0,0 +1,13 @@ +insert into project_detail ( + project_detail_id + , project_id + , project_number + , project_term_number + , projec_category) +values ( + /* detail.projectDetailId */1, + , /* detail.projectId */1 + , /* detail.projectNumber */1 + , /* detail.getTermNumber() */'term' + , /* detail.projectCategory */'category' + ) \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpsToClassMethodDefinition.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpsToClassMethodDefinition.sql new file mode 100644 index 00000000..49cfaf47 --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/jumpsToClassMethodDefinition.sql @@ -0,0 +1,7 @@ +insert into project_manager ( + project_manager_id + , manager_name) +values ( + /* employee.getFirstProject().projectId */1, + /* employee.userName */'name' + ) \ No newline at end of file diff --git a/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/notDisplayGutterWithNonExistentDaoMethod.sql b/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/notDisplayGutterWithNonExistentDaoMethod.sql new file mode 100644 index 00000000..fd8c97be --- /dev/null +++ b/src/test/testData/src/main/resources/META-INF/doma/example/dao/gutteraction/JumpActionTestDao/notDisplayGutterWithNonExistentDaoMethod.sql @@ -0,0 +1,2 @@ +-- Tested that the corresponding Dao method does not exist and the gutter and action are not displayed. +select * from emp where id = /* id */1 \ No newline at end of file