├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── auto_release.yaml │ ├── release.yaml │ ├── spellcheck.yaml │ └── tests.yaml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CONTRIBUTING.md ├── Chickensoft.AutoInject.Analyzers.Tests ├── Chickensoft.AutoInject.Analyzers.Tests.csproj └── test │ ├── src │ └── fixes │ │ ├── AutoInjectNotificationOverrideFixProviderTest.cs │ │ ├── AutoInjectNotifyMissingFixProviderTest.cs │ │ └── AutoInjectProvideFixProviderTest.cs │ ├── util │ └── AssemblyHelper.cs │ └── verifiers │ ├── CSAnalyzer+Test.cs │ ├── CSAnalyzer.cs │ ├── CSCodeFix+Test.cs │ ├── CSCodeFix.cs │ └── CSHelper.cs ├── Chickensoft.AutoInject.Analyzers ├── Chickensoft.AutoInject.Analyzers.csproj ├── README.md └── src │ ├── AutoInjectNotificationOverrideMissingAnalyzer.cs │ ├── AutoInjectNotifyMissingAnalyzer.cs │ ├── AutoInjectProvideAnalyzer.cs │ ├── fixes │ ├── AutoInjectNotificationOverrideFixProvider.cs │ ├── AutoInjectNotifyMissingFixProvider.cs │ └── AutoInjectProvideFixProvider.cs │ └── utils │ ├── Constants.cs │ ├── Diagnostics.cs │ └── MethodModifier.cs ├── Chickensoft.AutoInject.Tests ├── Chickensoft.AutoInject.Tests.csproj ├── Chickensoft.AutoInject.Tests.sln ├── badges │ ├── .gdignore │ ├── branch_coverage.svg │ └── line_coverage.svg ├── coverage.sh ├── coverage │ └── .gdignore ├── icon.svg ├── icon.svg.import ├── project.godot ├── src │ ├── auto_connect │ │ ├── AutoConnectExtensions.cs │ │ ├── AutoConnector.cs │ │ ├── IAutoConnect.cs │ │ └── NodeAttribute.cs │ ├── auto_init │ │ └── IAutoInit.cs │ ├── auto_inject │ │ ├── dependent │ │ │ ├── DependencyAttribute.cs │ │ │ ├── DependencyExceptions.cs │ │ │ ├── DependencyResolver.cs │ │ │ ├── DependentExtensions.cs │ │ │ ├── DependentState.cs │ │ │ ├── IDependent.cs │ │ │ └── PendingProvider.cs │ │ └── provider │ │ │ ├── IProvide.cs │ │ │ ├── IProvider.cs │ │ │ ├── ProviderExtensions.cs │ │ │ └── ProviderState.cs │ ├── auto_node │ │ └── AutoNode.cs │ ├── auto_on │ │ └── IAutoOn.cs │ ├── misc │ │ └── IReadyAware.cs │ └── notifications │ │ ├── NotificationExtensions.cs │ │ └── NotificationState.cs └── test │ ├── Tests.cs │ ├── Tests.tscn │ ├── fixtures │ ├── AutoConnectInvalidCastTestScene.cs │ ├── AutoConnectInvalidCastTestScene.tscn │ ├── AutoConnectMissingTestScene.cs │ ├── AutoConnectMissingTestScene.tscn │ ├── AutoConnectTestScene.cs │ ├── AutoConnectTestScene.tscn │ ├── AutoSetupTestNode.cs │ ├── Dependents.cs │ ├── MultiProvider.cs │ ├── MultiProvider.tscn │ ├── MyNode.cs │ ├── OtherAttribute.cs │ └── Providers.cs │ └── src │ ├── AutoConnectInvalidCastTest.cs │ ├── AutoConnectMissingTest.cs │ ├── AutoConnectTest.cs │ ├── AutoInitTest.cs │ ├── AutoNodeTest.cs │ ├── AutoOnTest.cs │ ├── FakeNodeTreeTest.cs │ ├── MiscTest.cs │ ├── MultiResolutionTest.cs │ ├── MyNodeTest.cs │ ├── NodeAttributeTest.cs │ ├── NotificationExtensionsTest.cs │ ├── ResolutionTest.cs │ └── SuperNodeTest.cs ├── Chickensoft.AutoInject.sln ├── Chickensoft.AutoInject ├── Chickensoft.AutoInject.csproj ├── icon.png ├── nupkg │ └── .gitkeep └── src │ └── .gitkeep ├── LICENSE ├── README.md ├── cspell.json ├── docs ├── renovatebot_pr.png └── spelling_fix.png ├── global.json ├── manual_build.sh ├── nuget.config └── renovate.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.bmp filter=lfs diff=lfs merge=lfs -text 3 | *.dll filter=lfs diff=lfs merge=lfs -text 4 | *.exe filter=lfs diff=lfs merge=lfs -text 5 | *.ico filter=lfs diff=lfs merge=lfs -text 6 | *.jpeg filter=lfs diff=lfs merge=lfs -text 7 | *.jpg filter=lfs diff=lfs merge=lfs -text 8 | *.png filter=lfs diff=lfs merge=lfs -text 9 | -------------------------------------------------------------------------------- /.github/workflows/auto_release.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will run whenever tests finish running. If tests pass, it will 2 | # look at the last commit message to see if it contains the phrase 3 | # "chore(deps): update all dependencies". 4 | # 5 | # If it finds a commit with that phrase, and the testing workflow has passed, 6 | # it will automatically release a new version of the project by running the 7 | # publish workflow. 8 | # 9 | # The commit message phrase above is always used by renovatebot when opening 10 | # PR's to update dependencies. If you have renovatebot enabled and set to 11 | # automatically merge in dependency updates, this can automatically release and 12 | # publish the updated version of the project. 13 | # 14 | # You can disable this action by setting the DISABLE_AUTO_RELEASE repository 15 | # variable to true. 16 | 17 | name: '🦾 Auto-Release' 18 | on: 19 | workflow_run: 20 | workflows: ["🚥 Tests"] 21 | branches: 22 | - main 23 | types: 24 | - completed 25 | 26 | jobs: 27 | auto_release: 28 | name: 🦾 Auto-Release 29 | runs-on: ubuntu-latest 30 | outputs: 31 | should_release: ${{ steps.release.outputs.should_release }} 32 | steps: 33 | - name: 🧾 Checkout 34 | uses: actions/checkout@v4 35 | with: 36 | lfs: true 37 | submodules: 'recursive' 38 | 39 | - name: 🧑‍🔬 Check Test Results 40 | id: tests 41 | run: | 42 | echo "passed=${{ github.event.workflow_run.conclusion == 'success' }}" >> "$GITHUB_OUTPUT" 43 | 44 | - name: 📄 Check If Dependencies Changed 45 | id: deps 46 | run: | 47 | message=$(git log -1 --pretty=%B) 48 | 49 | if [[ $message == *"chore(deps)"* ]]; then 50 | echo "changed=true" >> "$GITHUB_OUTPUT" 51 | else 52 | echo "changed=false" >> "$GITHUB_OUTPUT" 53 | fi 54 | 55 | - name: 📝 Check Release Status 56 | id: release 57 | run: | 58 | echo "Tests passed: ${{ steps.tests.outputs.passed }}" 59 | echo "Dependencies changed: ${{ steps.deps.outputs.changed }}" 60 | disable_auto_release='${{ vars.DISABLE_AUTO_RELEASE }}' 61 | echo "DISABLE_AUTO_RELEASE=$disable_auto_release" 62 | 63 | if [[ ${{ steps.tests.outputs.passed }} == "true" && ${{ steps.deps.outputs.changed }} == "true" && $disable_auto_release != "true" ]]; then 64 | echo "should_release=true" >> "$GITHUB_OUTPUT" 65 | echo "🦾 Creating a release!" 66 | else 67 | echo "should_release=false" >> "$GITHUB_OUTPUT" 68 | echo "✋ Not creating a release." 69 | fi 70 | 71 | trigger_release: 72 | uses: './.github/workflows/release.yaml' 73 | needs: auto_release 74 | if: needs.auto_release.outputs.should_release == 'true' 75 | secrets: 76 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} 77 | GH_BASIC: ${{ secrets.GH_BASIC }} 78 | with: 79 | bump: patch 80 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: "📦 Release" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | bump: 6 | description: "version bump method: major, minor, patch" 7 | type: choice 8 | options: 9 | - major 10 | - minor 11 | - patch 12 | required: true 13 | default: patch 14 | # Make a release whenever we're told to by another workflow. 15 | workflow_call: 16 | secrets: 17 | NUGET_API_KEY: 18 | description: "API key for Nuget" 19 | required: true 20 | GH_BASIC: 21 | description: "Personal access token (PAT) for GitHub" 22 | required: true 23 | # Input unifies with the workflow dispatch since it's identical. 24 | inputs: 25 | bump: 26 | type: string 27 | description: "major, minor, or patch" 28 | required: true 29 | default: "patch" 30 | jobs: 31 | publish: 32 | name: 📦 Release 33 | runs-on: ubuntu-latest 34 | if: github.repository == 'chickensoft-games/AutoInject' 35 | steps: 36 | - name: 🧾 Checkout 37 | uses: actions/checkout@v4 38 | with: 39 | token: ${{ secrets.GH_BASIC }} 40 | lfs: true 41 | submodules: "recursive" 42 | fetch-depth: 0 # So we can get all tags. 43 | 44 | - name: 🔎 Read Current Project Version 45 | id: current-version 46 | run: | 47 | echo "tag=$(git tag --sort=v:refname | grep -E '^[^v]' | tail -1)" >> "$GITHUB_OUTPUT" 48 | 49 | - name: 🖨 Print Current Version 50 | run: | 51 | echo "Current Version: ${{ steps.current-version.outputs.tag }}" 52 | 53 | - name: 🧮 Compute Next Version 54 | uses: chickensoft-games/next-godot-csproj-version@v1 55 | id: next-version 56 | with: 57 | project-version: ${{ steps.current-version.outputs.tag }} 58 | godot-version: global.json 59 | bump: ${{ inputs.bump }} 60 | 61 | - name: ✨ Print Next Version 62 | run: | 63 | echo "Next Version: ${{ steps.next-version.outputs.version }}" 64 | 65 | # Write version to file so .NET will build correct version. 66 | - name: 📝 Write Version to File 67 | uses: jacobtomlinson/gha-find-replace@v3 68 | with: 69 | find: "0.0.0-devbuild" 70 | replace: ${{ steps.next-version.outputs.version }} 71 | regex: false 72 | include: Chickensoft.AutoInject*/Chickensoft.AutoInject*.csproj 73 | 74 | - name: 🖨 Copy Source to Source-Only package 75 | run: | 76 | # Copy source files from Chickensoft.AutoInject.Tests/src/**/*.cs 77 | # to Chickensoft.AutoInject/src/**/*.cs 78 | # 79 | # Because source-only packages are hard to develop and test, we 80 | # actually keep the source that goes in the source-only package inside 81 | # the test project to make it easier to develop and test. 82 | # 83 | # we can always copy it right before publishing the package. 84 | 85 | mkdir -p Chickensoft.AutoInject/src 86 | cp -v -r Chickensoft.AutoInject.Tests/src/* Chickensoft.AutoInject/src/ 87 | 88 | - name: 🤐 Suppress Warnings From Files 89 | run: | 90 | # Define the multiline prefix and suffix 91 | PREFIX="#pragma warning disable 92 | #nullable enable 93 | " 94 | SUFFIX=" 95 | #nullable restore 96 | #pragma warning restore" 97 | 98 | # Function to add prefix and suffix to a file 99 | add_prefix_suffix() { 100 | local file="$1" 101 | # Create a temporary file 102 | tmp_file=$(mktemp) 103 | 104 | # Add prefix, content of the file, and suffix to the temporary file 105 | { 106 | echo "$PREFIX" 107 | cat "$file" 108 | echo "$SUFFIX" 109 | } > "$tmp_file" 110 | 111 | # Move the temporary file to the original file 112 | mv "$tmp_file" "$file" 113 | } 114 | 115 | # Export the function and variables so they can be used by find 116 | export -f add_prefix_suffix 117 | export PREFIX 118 | export SUFFIX 119 | 120 | # Find all files and apply the function 121 | find Chickensoft.AutoInject/src -type f -name "*.cs" -exec bash -c 'add_prefix_suffix "$0"' {} \; 122 | 123 | - name: 💽 Setup .NET SDK 124 | uses: actions/setup-dotnet@v4 125 | with: 126 | # Use the .NET SDK from global.json in the root of the repository. 127 | global-json-file: global.json 128 | 129 | - name: 🛠 Build Source-Only Package 130 | working-directory: Chickensoft.AutoInject 131 | run: | 132 | dotnet build -c Release 133 | 134 | - name: 🛠 Build Analyzers 135 | working-directory: Chickensoft.AutoInject.Analyzers 136 | run: | 137 | dotnet build -c Release 138 | 139 | - name: 🔎 Get Package Path 140 | id: package-path 141 | run: | 142 | package=$(find ./Chickensoft.AutoInject/nupkg -name "*.nupkg") 143 | echo "package=$package" >> "$GITHUB_OUTPUT" 144 | echo "📦 Found package: $package" 145 | 146 | - name: 🔎 Get Analyzer Package Path 147 | id: analyzer-package-path 148 | run: | 149 | package=$(find ./Chickensoft.AutoInject.Analyzers/nupkg -name "*.nupkg") 150 | echo "package=$package" >> "$GITHUB_OUTPUT" 151 | echo "📦 Found package: $package" 152 | 153 | - name: ✨ Create Release 154 | env: 155 | GITHUB_TOKEN: ${{ secrets.GH_BASIC }} 156 | run: | 157 | version="${{ steps.next-version.outputs.version }}" 158 | gh release create --title "v$version" --generate-notes "$version" \ 159 | "${{ steps.package-path.outputs.package }}" \ 160 | "${{ steps.analyzer-package-path.outputs.package }}" 161 | 162 | - name: 🛜 Publish to Nuget 163 | run: | 164 | dotnet nuget push "${{ steps.package-path.outputs.package }}" \ 165 | --api-key "${{ secrets.NUGET_API_KEY }}" \ 166 | --source "https://api.nuget.org/v3/index.json" --skip-duplicate 167 | 168 | - name: 🛜 Publish to Nuget 169 | run: | 170 | dotnet nuget push "${{ steps.package-path.outputs.package }}" \ 171 | --api-key "${{ secrets.NUGET_API_KEY }}" \ 172 | --source "https://api.nuget.org/v3/index.json" --skip-duplicate 173 | dotnet nuget push "${{ steps.analyzer-package-path.outputs.package }}" \ 174 | --api-key "${{ secrets.NUGET_API_KEY }}" \ 175 | --source "https://api.nuget.org/v3/index.json" --skip-duplicate 176 | -------------------------------------------------------------------------------- /.github/workflows/spellcheck.yaml: -------------------------------------------------------------------------------- 1 | name: '🧑‍🏫 Spellcheck' 2 | on: 3 | push: 4 | pull_request: 5 | 6 | jobs: 7 | spellcheck: 8 | name: '🧑‍🏫 Spellcheck' 9 | # Only run the workflow if it's not a PR or if it's a PR from a fork. 10 | # This prevents duplicate workflows from running on PR's that originate 11 | # from the repository itself. 12 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name 13 | runs-on: ubuntu-latest 14 | defaults: 15 | run: 16 | working-directory: '.' 17 | steps: 18 | - uses: actions/checkout@v4 19 | name: 🧾 Checkout 20 | 21 | - uses: streetsidesoftware/cspell-action@v7 22 | name: 📝 Check Spelling 23 | with: 24 | config: './cspell.json' 25 | incremental_files_only: false 26 | root: '.' 27 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: 🚥 Tests 2 | on: 3 | push: 4 | pull_request: 5 | 6 | jobs: 7 | tests: 8 | name: 🧪 Evaluate Tests on ${{ matrix.os }} 9 | # Only run the workflow if it's not a PR or if it's a PR from a fork. 10 | # This prevents duplicate workflows from running on PR's that originate 11 | # from the repository itself. 12 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | # Don't cancel other OS runners if one fails. 16 | fail-fast: false 17 | matrix: 18 | # Put the operating systems you want to run on here. 19 | os: [ubuntu-latest] 20 | env: 21 | DOTNET_CLI_TELEMETRY_OPTOUT: true 22 | DOTNET_NOLOGO: true 23 | defaults: 24 | run: 25 | # Use bash shells on all platforms. 26 | shell: bash 27 | steps: 28 | - name: 🧾 Checkout 29 | uses: actions/checkout@v4 30 | 31 | - name: 💽 Setup .NET SDK 32 | uses: actions/setup-dotnet@v4 33 | with: 34 | # Use the .NET SDK from global.json in the root of the repository. 35 | global-json-file: global.json 36 | 37 | - name: 📦 Restore Dependencies 38 | run: dotnet restore 39 | 40 | - name: 🤖 Setup Godot 41 | uses: chickensoft-games/setup-godot@v2 42 | with: 43 | # Version must include major, minor, and patch, and be >= 4.0.0 44 | # Pre-release label is optional. 45 | # 46 | # In this case, we are using the version from global.json. 47 | # 48 | # This allows checks on renovatebot PR's to succeed whenever 49 | # renovatebot updates the Godot SDK version. 50 | version: global.json 51 | 52 | - name: 🔬 Verify Setup 53 | run: | 54 | dotnet --version 55 | godot --version 56 | 57 | - name: 🧑‍🔬 Generate .NET Bindings 58 | working-directory: Chickensoft.AutoInject.Tests 59 | run: godot --headless --build-solutions --quit || exit 0 60 | 61 | - name: 🦺 Build Projects 62 | run: dotnet build 63 | 64 | - name: 🧪 Run Tests 65 | working-directory: Chickensoft.AutoInject.Tests 66 | run: godot --headless --run-tests --quit-on-finish 67 | 68 | - name: 🧪 Run Analyzer Tests 69 | working-directory: Chickensoft.AutoInject.Analyzers.Tests 70 | run: dotnet test 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Chickensoft.AutoInject.Tests/coverage/* 2 | !Chickensoft.AutoInject.Tests/coverage/.gdignore 3 | 4 | .godot/ 5 | bin/ 6 | obj/ 7 | .generated/ 8 | .vs/ 9 | .idea/ 10 | .DS_Store 11 | nupkg/ 12 | !Chickensoft.AutoInject/nupkg/.gitkeep 13 | 14 | # User-specific files 15 | *.suo 16 | *.user 17 | *.userosscache 18 | *.sln.docstates 19 | 20 | # User-specific files (MonoDevelop/Xamarin Studio) 21 | *.userprefs 22 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-dotnettools.csharp", 4 | "selcukermaya.se-csproj-extensions", 5 | "josefpihrt-vscode.roslynator", 6 | "streetsidesoftware.code-spell-checker", 7 | "VisualStudioExptTeam.vscodeintellicode", 8 | "DavidAnson.vscode-markdownlint" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | // For these launch configurations to work, you need to setup a GODOT 5 | // environment variable. On mac or linux, this can be done by adding 6 | // the following to your .zshrc, .bashrc, or .bash_profile file: 7 | // export GODOT="/Applications/Godot.app/Contents/MacOS/Godot" 8 | { 9 | "name": "🧪 Debug Tests", 10 | "type": "coreclr", 11 | "request": "launch", 12 | "preLaunchTask": "build", 13 | "program": "${env:GODOT}", 14 | "args": [ 15 | "--headless", 16 | // These command line flags are used by GoDotTest to run tests. 17 | "--run-tests", 18 | "--quit-on-finish" 19 | ], 20 | "cwd": "${workspaceFolder}/Chickensoft.AutoInject.Tests", 21 | "stopAtEntry": false, 22 | }, 23 | { 24 | "name": "🔬 Debug Current Test", 25 | "type": "coreclr", 26 | "request": "launch", 27 | "preLaunchTask": "build", 28 | "program": "${env:GODOT}", 29 | "args": [ 30 | "--headless", 31 | // These command line flags are used by GoDotTest to run tests. 32 | "--run-tests=${fileBasenameNoExtension}", 33 | "--quit-on-finish" 34 | ], 35 | "cwd": "${workspaceFolder}/Chickensoft.AutoInject.Tests", 36 | "stopAtEntry": false, 37 | }, 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[csharp]": { 3 | "editor.codeActionsOnSave": { 4 | "source.addMissingImports": "explicit", 5 | "source.fixAll": "explicit", 6 | "source.organizeImports": "explicit" 7 | }, 8 | "editor.formatOnPaste": true, 9 | "editor.formatOnSave": true, 10 | "editor.formatOnType": false 11 | }, 12 | // Required to keep the C# language server from getting confused about which 13 | // solution to open. 14 | "dotnet.defaultSolution": "Chickensoft.AutoInject.sln", 15 | "csharp.semanticHighlighting.enabled": true, 16 | "editor.semanticHighlighting.enabled": true, 17 | "editor.rulers": [80, 120], 18 | // C# doc comment colorization gets lost with semantic highlighting, but we 19 | // need semantic highlighting for proper syntax highlighting with record 20 | // shorthand. 21 | // 22 | // Here's a workaround for doc comment highlighting from 23 | // https://github.com/OmniSharp/omnisharp-vscode/issues/3816 24 | "editor.tokenColorCustomizations": { 25 | "[*Dark*]": { 26 | // Themes that include the word "Dark" in them. 27 | "textMateRules": [ 28 | { 29 | "scope": "comment.documentation", 30 | "settings": { 31 | "foreground": "#608B4E" 32 | } 33 | }, 34 | { 35 | "scope": "comment.documentation.attribute", 36 | "settings": { 37 | "foreground": "#C8C8C8" 38 | } 39 | }, 40 | { 41 | "scope": "comment.documentation.cdata", 42 | "settings": { 43 | "foreground": "#E9D585" 44 | } 45 | }, 46 | { 47 | "scope": "comment.documentation.delimiter", 48 | "settings": { 49 | "foreground": "#808080" 50 | } 51 | }, 52 | { 53 | "scope": "comment.documentation.name", 54 | "settings": { 55 | "foreground": "#569CD6" 56 | } 57 | } 58 | ] 59 | }, 60 | "[*Light*]": { 61 | // Themes that include the word "Light" in them. 62 | "textMateRules": [ 63 | { 64 | "scope": "comment.documentation", 65 | "settings": { 66 | "foreground": "#008000" 67 | } 68 | }, 69 | { 70 | "scope": "comment.documentation.attribute", 71 | "settings": { 72 | "foreground": "#282828" 73 | } 74 | }, 75 | { 76 | "scope": "comment.documentation.cdata", 77 | "settings": { 78 | "foreground": "#808080" 79 | } 80 | }, 81 | { 82 | "scope": "comment.documentation.delimiter", 83 | "settings": { 84 | "foreground": "#808080" 85 | } 86 | }, 87 | { 88 | "scope": "comment.documentation.name", 89 | "settings": { 90 | "foreground": "#808080" 91 | } 92 | } 93 | ] 94 | }, 95 | "[*]": { 96 | // Themes that don't include the word "Dark" or "Light" in them. 97 | // These are some bold colors that show up well against most dark and 98 | // light themes. 99 | // 100 | // Change them to something that goes well with your preferred theme :) 101 | "textMateRules": [ 102 | { 103 | "scope": "comment.documentation", 104 | "settings": { 105 | "foreground": "#0091ff" 106 | } 107 | }, 108 | { 109 | "scope": "comment.documentation.attribute", 110 | "settings": { 111 | "foreground": "#8480ff" 112 | } 113 | }, 114 | { 115 | "scope": "comment.documentation.cdata", 116 | "settings": { 117 | "foreground": "#0091ff" 118 | } 119 | }, 120 | { 121 | "scope": "comment.documentation.delimiter", 122 | "settings": { 123 | "foreground": "#aa00ff" 124 | } 125 | }, 126 | { 127 | "scope": "comment.documentation.name", 128 | "settings": { 129 | "foreground": "#ef0074" 130 | } 131 | } 132 | ] 133 | } 134 | }, 135 | "markdownlint.config": { 136 | // Allow html in markdown. 137 | "MD033": false, 138 | // Allow non-unique heading names so we don't break the changelog. 139 | "MD024": false 140 | }, 141 | "markdownlint.ignore": [ 142 | "**/LICENSE" 143 | ], 144 | // Remove these if you're happy with your terminal profiles. 145 | "terminal.integrated.defaultProfile.windows": "Git Bash", 146 | "terminal.integrated.profiles.windows": { 147 | "Command Prompt": { 148 | "icon": "terminal-cmd", 149 | "path": [ 150 | "${env:windir}\\Sysnative\\cmd.exe", 151 | "${env:windir}\\System32\\cmd.exe" 152 | ] 153 | }, 154 | "Git Bash": { 155 | "icon": "terminal", 156 | "source": "Git Bash" 157 | }, 158 | "PowerShell": { 159 | "icon": "terminal-powershell", 160 | "source": "PowerShell" 161 | } 162 | }, 163 | "dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true 164 | } 165 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "--no-restore" 11 | ], 12 | "problemMatcher": "$msCompile", 13 | "presentation": { 14 | "echo": true, 15 | "reveal": "silent", 16 | "focus": false, 17 | "panel": "shared", 18 | "showReuseMessage": false, 19 | "clear": false 20 | } 21 | }, 22 | { 23 | "label": "coverage", 24 | "group": "test", 25 | "command": "${workspaceFolder}/Chickensoft.AutoInject.Tests/coverage.sh", 26 | "type": "shell", 27 | "options": { 28 | "cwd": "${workspaceFolder}/Chickensoft.AutoInject.Tests" 29 | }, 30 | "presentation": { 31 | "echo": true, 32 | "reveal": "always", 33 | "focus": false, 34 | "panel": "shared", 35 | "showReuseMessage": false, 36 | "clear": true 37 | }, 38 | }, 39 | { 40 | "label": "build-solutions", 41 | "group": "test", 42 | "command": "dotnet restore; ${env:GODOT} --headless --build-solutions --quit || exit 0", 43 | "type": "shell", 44 | "options": { 45 | "cwd": "${workspaceFolder}/Chickensoft.AutoInject.Tests" 46 | }, 47 | "presentation": { 48 | "echo": true, 49 | "reveal": "silent", 50 | "focus": false, 51 | "panel": "shared", 52 | "showReuseMessage": false, 53 | "clear": false 54 | } 55 | }, 56 | { 57 | "label": "test-analyzers", 58 | "group": "test", 59 | "command": "dotnet", 60 | "type": "shell", 61 | "options": { 62 | "cwd": "${workspaceFolder}/Chickensoft.AutoInject.Analyzers.Tests", 63 | }, 64 | "args": [ 65 | "test" 66 | ], 67 | "presentation": { 68 | "echo": true, 69 | "reveal": "always", 70 | "focus": false, 71 | "panel": "shared", 72 | "showReuseMessage": true, 73 | "clear": false 74 | }, 75 | "problemMatcher": "$msCompile" 76 | }, 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for taking the time to read this contributing guide and for showing interest in helping this project! 4 | 5 | ## Getting Started 6 | 7 | Need a helping hand to get started? Check out these resources! 8 | 9 | - [Discord Server][discord] 10 | - [Chickensoft Website][chickensoft] 11 | 12 | Please read our [code of conduct](#code-of-conduct). We do our best to treat others fairly and foster a welcoming environment. 13 | 14 | ## Project Setup 15 | 16 | This is a C# nuget package, for use with the .NET SDK 6 or 7. As such, the `dotnet` tool will allow you to restore packages and build projects. 17 | 18 | The `Chickensoft.AutoInject.Tests` project must be built with the Godot editor at least once before `dotnet build` will succeed. Godot has to generate the .NET bindings for the project, since tests run in an actual game environment. 19 | 20 | ## Coding Guidelines 21 | 22 | Your IDE should automatically adhere to the style guidelines in the provided `.editorconfig` file. Please try to keep lines under 80 characters long whenever possible. 23 | 24 | We try to write tests for our projects to ensure a certain level of quality. We are willing to give you support and guidance if you need help! 25 | 26 | ## Code of Conduct 27 | 28 | We follow the [Contributor Covenant][covenant]. 29 | 30 | In short: 31 | 32 | > We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. 33 | 34 | 35 | 36 | [discord]: https://discord.gg/gSjaPgMmYW 37 | [chickensoft]: https://chickensoft.games 38 | [covenant]: https://www.contributor-covenant.org/version/2/1/code_of_conduct/ 39 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers.Tests/Chickensoft.AutoInject.Analyzers.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | disable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | $(MSBuildProjectDirectory) 16 | $([MSBuild]::ConvertToBase64('$(GodotProjectDir)')) 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers.Tests/test/src/fixes/AutoInjectNotificationOverrideFixProviderTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Analyzers.Tests; 2 | 3 | using System.Threading.Tasks; 4 | using Chickensoft.AutoInject.Analyzers.Utils; 5 | using Xunit; 6 | using VerifyCS = Verifiers.CSharpCodeFixVerifier; 7 | 8 | public class AutoInjectNotificationOverrideFixProviderTest { 9 | [Fact] 10 | public async Task DoesNotOfferDiagnosticIfNotificationOverrideExists() { 11 | var diagnosticID = Diagnostics 12 | .MissingAutoInjectNotificationOverrideDescriptor 13 | .Id; 14 | 15 | var testCode = $$""" 16 | using Chickensoft.AutoInject; 17 | using Chickensoft.Introspection; 18 | using Godot; 19 | 20 | [Meta(typeof(IAutoNode))] 21 | partial class MyNode : Node 22 | { 23 | public override void _Notification(int what) { } 24 | } 25 | """; 26 | 27 | await VerifyCS.VerifyAnalyzerAsync( 28 | testCode.ReplaceLineEndings() 29 | ); 30 | } 31 | 32 | [Fact] 33 | public async Task FixesMissingNotificationOverrideByAddingOverrideWithNotify() { 34 | var diagnosticID = Diagnostics 35 | .MissingAutoInjectNotificationOverrideDescriptor 36 | .Id; 37 | 38 | var testCode = $$""" 39 | using Chickensoft.AutoInject; 40 | using Chickensoft.Introspection; 41 | using Godot; 42 | 43 | [{|{{diagnosticID}}:Meta(typeof(IAutoNode))|}] 44 | partial class MyNode : Node 45 | { 46 | } 47 | """; 48 | 49 | var fixedCode = $$""" 50 | using Chickensoft.AutoInject; 51 | using Chickensoft.Introspection; 52 | using Godot; 53 | 54 | [Meta(typeof(IAutoNode))] 55 | partial class MyNode : Node 56 | { 57 | public override void _Notification(int what) => this.Notify(what); 58 | } 59 | """; 60 | 61 | await VerifyCS.VerifyCodeFixAsync( 62 | testCode.ReplaceLineEndings(), 63 | fixedCode.ReplaceLineEndings() 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers.Tests/test/src/fixes/AutoInjectNotifyMissingFixProviderTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Analyzers.Tests; 2 | 3 | using System.Threading.Tasks; 4 | using Chickensoft.AutoInject.Analyzers.Utils; 5 | using Xunit; 6 | using VerifyCS = Verifiers.CSharpCodeFixVerifier; 7 | 8 | public class AutoInjectNotifyMissingFixProviderTest { 9 | [Fact] 10 | public async Task DoesNotOfferDiagnosticIfNotificationOverrideExistsAndCallsNotify() { 11 | var diagnosticID = Diagnostics 12 | .MissingAutoInjectNotificationOverrideDescriptor 13 | .Id; 14 | 15 | var testCode = $$""" 16 | using Chickensoft.AutoInject; 17 | using Chickensoft.Introspection; 18 | using Godot; 19 | 20 | [Meta(typeof(IAutoNode))] 21 | partial class MyNode : Node 22 | { 23 | public override void _Notification(int what) { this.Notify(what); } 24 | } 25 | """; 26 | 27 | await VerifyCS.VerifyAnalyzerAsync( 28 | testCode.ReplaceLineEndings() 29 | ); 30 | } 31 | 32 | [Fact] 33 | public async Task DoesNotOfferDiagnosticIfNotifyCallExistsOutsideOverride() { 34 | var diagnosticID = Diagnostics 35 | .MissingAutoInjectNotificationOverrideDescriptor 36 | .Id; 37 | 38 | var testCode = $$""" 39 | using Chickensoft.AutoInject; 40 | using Chickensoft.Introspection; 41 | using Godot; 42 | 43 | [Meta(typeof(IAutoNode))] 44 | partial class MyNode : Node 45 | { 46 | public void SomeHelperMethod(int what) { this.Notify(what); } 47 | } 48 | """; 49 | 50 | await VerifyCS.VerifyAnalyzerAsync( 51 | testCode.ReplaceLineEndings() 52 | ); 53 | } 54 | 55 | [Fact] 56 | public async Task FixesMissingNotifyCallByAddingToOverride() { 57 | var diagnosticID = Diagnostics 58 | .MissingAutoInjectNotifyDescriptor 59 | .Id; 60 | 61 | var testCode = $$""" 62 | using Chickensoft.AutoInject; 63 | using Chickensoft.Introspection; 64 | using Godot; 65 | 66 | [{|{{diagnosticID}}:Meta(typeof(IAutoNode))|}] 67 | partial class MyNode : Node 68 | { 69 | public override void _Notification(int what) { } 70 | } 71 | """; 72 | 73 | var fixedCode = $$""" 74 | using Chickensoft.AutoInject; 75 | using Chickensoft.Introspection; 76 | using Godot; 77 | 78 | [Meta(typeof(IAutoNode))] 79 | partial class MyNode : Node 80 | { 81 | public override void _Notification(int what) 82 | { 83 | this.Notify(what); 84 | } 85 | } 86 | """; 87 | 88 | await VerifyCS.VerifyCodeFixAsync( 89 | testCode.ReplaceLineEndings(), 90 | fixedCode.ReplaceLineEndings() 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers.Tests/test/util/AssemblyHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Analyzers.Tests.Util; 2 | 3 | using System; 4 | using System.IO; 5 | 6 | public static class AssemblyHelper { 7 | /// 8 | /// Get the path to the assembly for a given type, without the file extension 9 | /// if one exists. 10 | /// 11 | /// A type belonging to the desired assembly. 12 | /// The path to the assembly, excluding any file extension. 13 | public static string GetAssemblyPath(Type type) { 14 | var path = type.Assembly.Location; 15 | var extension = Path.GetExtension(path); 16 | return path[..^extension.Length]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers.Tests/test/verifiers/CSAnalyzer+Test.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Analyzers.Tests.Verifiers; 2 | 3 | using Microsoft.CodeAnalysis.CSharp.Testing; 4 | using Microsoft.CodeAnalysis.Diagnostics; 5 | using Microsoft.CodeAnalysis.Testing; 6 | 7 | public static partial class CSharpAnalyzerVerifier 8 | where TAnalyzer : DiagnosticAnalyzer, new() { 9 | public class Test : CSharpAnalyzerTest { 10 | public Test() { 11 | SolutionTransforms.Add( 12 | (solution, projectId) => { 13 | var project = solution.GetProject(projectId); 14 | if (project is null) { 15 | return solution; 16 | } 17 | var compilationOptions = project.CompilationOptions; 18 | if (compilationOptions is null) { 19 | return solution; 20 | } 21 | compilationOptions = compilationOptions.WithSpecificDiagnosticOptions( 22 | compilationOptions 23 | .SpecificDiagnosticOptions 24 | .SetItems(CSharpVerifierHelper.NullableWarnings) 25 | ); 26 | solution = solution.WithProjectCompilationOptions( 27 | projectId, 28 | compilationOptions 29 | ); 30 | return solution; 31 | } 32 | ); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers.Tests/test/verifiers/CSAnalyzer.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Analyzers.Tests.Verifiers; 2 | 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Chickensoft.AutoInject; 6 | using Chickensoft.AutoInject.Analyzers.Tests.Util; 7 | using Godot; 8 | using Microsoft.CodeAnalysis; 9 | using Microsoft.CodeAnalysis.CSharp.Testing; 10 | using Microsoft.CodeAnalysis.Diagnostics; 11 | using Microsoft.CodeAnalysis.Testing; 12 | 13 | [System.Diagnostics.CodeAnalysis.SuppressMessage( 14 | "Design", 15 | "CA1000: Do not declare static members on generic types", 16 | Justification = "CA1000 prefers no generic arguments, but either method or class needs them here" 17 | )] 18 | public static partial class CSharpAnalyzerVerifier 19 | where TAnalyzer : DiagnosticAnalyzer, new() { 20 | /// 21 | public static DiagnosticResult Diagnostic() 22 | => CSharpAnalyzerVerifier.Diagnostic(); 23 | 24 | /// 25 | public static DiagnosticResult Diagnostic(string diagnosticId) 26 | => CSharpAnalyzerVerifier 27 | .Diagnostic(diagnosticId); 28 | 29 | /// 30 | public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) 31 | => CSharpAnalyzerVerifier 32 | .Diagnostic(descriptor); 33 | 34 | public static Test CreateTest(string source) { 35 | var test = new Test { 36 | TestCode = source, 37 | }; 38 | 39 | var autoInjectAssemblyPath = 40 | AssemblyHelper.GetAssemblyPath(typeof(IAutoNode)); 41 | var introspectionAssemblyPath = 42 | AssemblyHelper.GetAssemblyPath(typeof(Introspection.MetaAttribute)); 43 | var godotAssemblyPath = 44 | AssemblyHelper.GetAssemblyPath(typeof(Node)); 45 | 46 | test.ReferenceAssemblies = ReferenceAssemblies.Net.Net80 47 | .AddAssemblies( 48 | [ 49 | autoInjectAssemblyPath, 50 | introspectionAssemblyPath, 51 | godotAssemblyPath, 52 | ] 53 | ); 54 | 55 | return test; 56 | } 57 | 58 | /// 59 | public static async Task VerifyAnalyzerAsync( 60 | string source, 61 | params DiagnosticResult[] expected) { 62 | var test = CreateTest(source); 63 | 64 | test.ExpectedDiagnostics.AddRange(expected); 65 | await test.RunAsync(CancellationToken.None); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers.Tests/test/verifiers/CSCodeFix+Test.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Analyzers.Tests.Verifiers; 2 | 3 | using Microsoft.CodeAnalysis.CodeFixes; 4 | using Microsoft.CodeAnalysis.CSharp.Testing; 5 | using Microsoft.CodeAnalysis.Diagnostics; 6 | using Microsoft.CodeAnalysis.Testing; 7 | 8 | public static partial class CSharpCodeFixVerifier 9 | where TAnalyzer : DiagnosticAnalyzer, new() 10 | where TCodeFix : CodeFixProvider, new() { 11 | public class Test : CSharpCodeFixTest { 12 | public Test() { 13 | SolutionTransforms.Add( 14 | (solution, projectId) => { 15 | var project = solution.GetProject(projectId); 16 | if (project is null) { 17 | return solution; 18 | } 19 | var compilationOptions = project.CompilationOptions; 20 | if (compilationOptions is null) { 21 | return solution; 22 | } 23 | compilationOptions = compilationOptions.WithSpecificDiagnosticOptions( 24 | compilationOptions 25 | .SpecificDiagnosticOptions 26 | .SetItems(CSharpVerifierHelper.NullableWarnings) 27 | ); 28 | solution = solution.WithProjectCompilationOptions( 29 | projectId, 30 | compilationOptions 31 | ); 32 | return solution; 33 | } 34 | ); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers.Tests/test/verifiers/CSCodeFix.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Analyzers.Tests.Verifiers; 2 | 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Chickensoft.AutoInject.Analyzers.Tests.Util; 6 | using Godot; 7 | using Microsoft.CodeAnalysis; 8 | using Microsoft.CodeAnalysis.CodeFixes; 9 | using Microsoft.CodeAnalysis.CSharp.Testing; 10 | using Microsoft.CodeAnalysis.Diagnostics; 11 | using Microsoft.CodeAnalysis.Testing; 12 | 13 | [System.Diagnostics.CodeAnalysis.SuppressMessage( 14 | "Design", 15 | "CA1000: Do not declare static members on generic types", 16 | Justification = "CA1000 prefers no generic arguments, but either method or class needs them here" 17 | )] 18 | public static partial class CSharpCodeFixVerifier 19 | where TAnalyzer : DiagnosticAnalyzer, new() 20 | where TCodeFix : CodeFixProvider, new() { 21 | /// 22 | public static DiagnosticResult Diagnostic() => 23 | CSharpCodeFixVerifier 24 | .Diagnostic(); 25 | 26 | /// 27 | public static DiagnosticResult Diagnostic(string diagnosticId) => 28 | CSharpCodeFixVerifier 29 | .Diagnostic(diagnosticId); 30 | 31 | /// 32 | public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) => 33 | CSharpCodeFixVerifier 34 | .Diagnostic(descriptor); 35 | 36 | public static Test CreateTest(string source, string? fixedSource = null) { 37 | var test = new Test { 38 | TestCode = source, 39 | }; 40 | 41 | if (fixedSource is not null) { 42 | test.FixedCode = fixedSource; 43 | } 44 | 45 | var autoInjectAssemblyPath = 46 | AssemblyHelper.GetAssemblyPath(typeof(IAutoNode)); 47 | var introspectionAssemblyPath = 48 | AssemblyHelper.GetAssemblyPath(typeof(Introspection.MetaAttribute)); 49 | var godotAssemblyPath = 50 | AssemblyHelper.GetAssemblyPath(typeof(Node)); 51 | 52 | test.ReferenceAssemblies = ReferenceAssemblies.Net.Net80 53 | .AddAssemblies( 54 | [ 55 | autoInjectAssemblyPath, 56 | introspectionAssemblyPath, 57 | godotAssemblyPath, 58 | ] 59 | ); 60 | 61 | return test; 62 | } 63 | 64 | /// 65 | public static async Task VerifyAnalyzerAsync( 66 | string source, 67 | params DiagnosticResult[] expected) { 68 | var test = CreateTest(source); 69 | 70 | test.ExpectedDiagnostics.AddRange(expected); 71 | await test.RunAsync(CancellationToken.None); 72 | } 73 | 74 | /// 75 | public static async Task VerifyCodeFixAsync( 76 | string source, 77 | string fixedSource, 78 | string? codeFixEquivalenceKey = null) => 79 | await VerifyCodeFixAsync( 80 | source, 81 | DiagnosticResult.EmptyDiagnosticResults, 82 | fixedSource, 83 | codeFixEquivalenceKey 84 | ); 85 | 86 | /// 87 | public static async Task VerifyCodeFixAsync( 88 | string source, 89 | DiagnosticResult expected, 90 | string fixedSource, 91 | string? codeFixEquivalenceKey = null) => 92 | await VerifyCodeFixAsync( 93 | source, 94 | [expected], 95 | fixedSource, 96 | codeFixEquivalenceKey 97 | ); 98 | 99 | /// 100 | public static async Task VerifyCodeFixAsync( 101 | string source, 102 | DiagnosticResult[] expected, 103 | string fixedSource, 104 | string? codeFixEquivalenceKey) { 105 | var test = CreateTest(source, fixedSource); 106 | 107 | if (codeFixEquivalenceKey is not null) { 108 | test.CodeActionEquivalenceKey = codeFixEquivalenceKey; 109 | } 110 | 111 | test.ExpectedDiagnostics.AddRange(expected); 112 | await test.RunAsync(CancellationToken.None); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers.Tests/test/verifiers/CSHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Analyzers.Tests.Verifiers; 2 | 3 | using System; 4 | using System.Collections.Immutable; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | 8 | internal static class CSharpVerifierHelper { 9 | /// 10 | /// By default, the compiler reports diagnostics for nullable reference types at 11 | /// , and the analyzer test framework defaults to only validating 12 | /// diagnostics at . This map contains all compiler diagnostic IDs 13 | /// related to nullability mapped to , which is then used to enable all 14 | /// of these warnings for default validation during analyzer and code fix tests. 15 | /// 16 | internal static ImmutableDictionary NullableWarnings { get; } = 17 | GetNullableWarningsFromCompiler(); 18 | 19 | private static ImmutableDictionary GetNullableWarningsFromCompiler() { 20 | string[] args = ["/warnaserror:nullable"]; 21 | var commandLineArguments = CSharpCommandLineParser.Default.Parse( 22 | args, 23 | baseDirectory: Environment.CurrentDirectory, 24 | sdkDirectory: Environment.CurrentDirectory 25 | ); 26 | var nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; 27 | 28 | // Workaround for https://github.com/dotnet/roslyn/issues/41610 29 | nullableWarnings = nullableWarnings 30 | .SetItem("CS8632", ReportDiagnostic.Error) 31 | .SetItem("CS8669", ReportDiagnostic.Error); 32 | 33 | return nullableWarnings; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers/Chickensoft.AutoInject.Analyzers.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | disable 5 | enable 6 | false 7 | true 8 | preview 9 | Chickensoft.AutoInject.Analyzers 10 | NU5128 11 | ./nupkg 12 | true 13 | cs 14 | true 15 | portable 16 | 17 | AutoInject Analyzers 18 | 0.0.0-devbuild 19 | Analyzers and code fixes for Chickensoft.AutoInject. 20 | © 2025 Chickensoft 21 | Chickensoft 22 | Chickensoft 23 | 24 | Chickensoft.AutoInject.Analyzers 25 | AutoInject Analyzers release. 26 | icon.png 27 | dependency injection; di; godot; chickensoft; nodes; analyzers; code fixes; 28 | README.md 29 | LICENSE 30 | https://github.com/chickensoft-games/AutoInject 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers/README.md: -------------------------------------------------------------------------------- 1 | # AutoInject Analyzers 2 | 3 | Roslyn analyzers providing diagnostics and code fixes for common issues using [Chickensoft.AutoInject](https://www.nuget.org/packages/Chickensoft.AutoInject) in Godot node scripts. 4 | 5 | Current diagnostics and fixes: 6 | * Missing override of `void _Notification(int what)` 7 | * Missing call to `this.Notify()` 8 | * Missing call to `this.Provide()` for nodes implementing `IProvider` 9 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers/src/AutoInjectNotificationOverrideMissingAnalyzer.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Analyzers; 2 | 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using Chickensoft.AutoInject.Analyzers.Utils; 6 | using Microsoft.CodeAnalysis; 7 | using Microsoft.CodeAnalysis.CSharp; 8 | using Microsoft.CodeAnalysis.CSharp.Syntax; 9 | using Microsoft.CodeAnalysis.Diagnostics; 10 | 11 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 12 | public class AutoInjectNotificationOverrideMissingAnalyzer : DiagnosticAnalyzer { 13 | public override ImmutableArray SupportedDiagnostics { 14 | get; 15 | } = [Diagnostics.MissingAutoInjectNotificationOverrideDescriptor]; 16 | 17 | public override void Initialize(AnalysisContext context) { 18 | context.EnableConcurrentExecution(); 19 | 20 | context.ConfigureGeneratedCodeAnalysis( 21 | GeneratedCodeAnalysisFlags.Analyze | 22 | GeneratedCodeAnalysisFlags.ReportDiagnostics 23 | ); 24 | 25 | context.RegisterSyntaxNodeAction( 26 | AnalyzeClassDeclaration, 27 | SyntaxKind.ClassDeclaration 28 | ); 29 | } 30 | 31 | private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) { 32 | var classDeclaration = (ClassDeclarationSyntax)context.Node; 33 | 34 | var attributes = classDeclaration.AttributeLists.SelectMany(list => list.Attributes 35 | ).Where(attribute => attribute.Name.ToString() == Constants.META_ATTRIBUTE_NAME 36 | // Check that Meta attribute has an AutoInject type (ex: [Meta(typeof(IAutoNode))]) 37 | && attribute.ArgumentList?.Arguments.Any(arg => 38 | arg.Expression is TypeOfExpressionSyntax { Type: IdentifierNameSyntax identifierName } && 39 | Constants.AutoInjectTypeNames.Contains(identifierName.Identifier.ValueText) 40 | ) == true 41 | ) 42 | .ToList(); 43 | 44 | if (attributes.Count == 0) { 45 | return; 46 | } 47 | 48 | // Check if the class has a _Notification override method. 49 | var hasNotificationOverride = classDeclaration 50 | .Members 51 | .OfType() 52 | .Any(method => 53 | method.Identifier.ValueText == "_Notification" && 54 | method.Modifiers.Any(SyntaxKind.OverrideKeyword) && 55 | method.ParameterList.Parameters.Count == 1 56 | ); 57 | 58 | if (!hasNotificationOverride) { 59 | // Report missing Notify call, _Notification override already exists. 60 | context.ReportDiagnostic( 61 | Diagnostics.MissingAutoInjectNotificationOverride( 62 | attributes[0].GetLocation(), 63 | classDeclaration.Identifier.ValueText 64 | ) 65 | ); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers/src/AutoInjectNotifyMissingAnalyzer.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Analyzers; 2 | 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using Chickensoft.AutoInject.Analyzers.Utils; 6 | using Microsoft.CodeAnalysis; 7 | using Microsoft.CodeAnalysis.CSharp; 8 | using Microsoft.CodeAnalysis.CSharp.Syntax; 9 | using Microsoft.CodeAnalysis.Diagnostics; 10 | 11 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 12 | public class AutoInjectNotifyMissingAnalyzer : DiagnosticAnalyzer { 13 | public override ImmutableArray SupportedDiagnostics { 14 | get; 15 | } = [Diagnostics.MissingAutoInjectNotifyDescriptor]; 16 | 17 | public override void Initialize(AnalysisContext context) { 18 | context.EnableConcurrentExecution(); 19 | 20 | context.ConfigureGeneratedCodeAnalysis( 21 | GeneratedCodeAnalysisFlags.Analyze | 22 | GeneratedCodeAnalysisFlags.ReportDiagnostics 23 | ); 24 | 25 | context.RegisterSyntaxNodeAction( 26 | AnalyzeClassDeclaration, 27 | SyntaxKind.ClassDeclaration 28 | ); 29 | } 30 | 31 | private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) { 32 | var classDeclaration = (ClassDeclarationSyntax)context.Node; 33 | 34 | var attributes = classDeclaration.AttributeLists.SelectMany(list => list.Attributes 35 | ).Where(attribute => attribute.Name.ToString() == Constants.META_ATTRIBUTE_NAME 36 | // Check that Meta attribute has an AutoInject type (ex: [Meta(typeof(IAutoNode))]) 37 | && attribute.ArgumentList?.Arguments.Any(arg => 38 | arg.Expression is TypeOfExpressionSyntax { Type: IdentifierNameSyntax identifierName } && 39 | Constants.AutoInjectTypeNames.Contains(identifierName.Identifier.ValueText) 40 | ) == true 41 | ) 42 | .ToList(); 43 | 44 | if (attributes.Count == 0) { 45 | return; 46 | } 47 | 48 | // Check if the class has a _Notification override method. 49 | // If it doesn't, the NotificationOverrideMissing analyzer will get it. 50 | var hasNotificationOverride = classDeclaration 51 | .Members 52 | .OfType() 53 | .Any( 54 | method => 55 | method.Identifier.ValueText == "_Notification" 56 | && method.Modifiers.Any(SyntaxKind.OverrideKeyword) 57 | && method.ParameterList.Parameters.Count == 1 58 | ); 59 | 60 | if (hasNotificationOverride) { 61 | // Check if the class calls "this.Notify()" in the _Notification override. 62 | var hasNotify = classDeclaration 63 | .DescendantNodes() 64 | .OfType() 65 | .Any(invocation => 66 | invocation.Expression is MemberAccessExpressionSyntax { 67 | Name.Identifier.ValueText: "Notify", 68 | Expression: ThisExpressionSyntax 69 | } 70 | ); 71 | 72 | if (!hasNotify) { 73 | // Report missing Notify call, _Notification override does not exist. 74 | context.ReportDiagnostic( 75 | Diagnostics.MissingAutoInjectNotify( 76 | attributes[0].GetLocation(), 77 | classDeclaration.Identifier.ValueText 78 | ) 79 | ); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers/src/AutoInjectProvideAnalyzer.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Analyzers; 2 | 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; 8 | using Microsoft.CodeAnalysis.Diagnostics; 9 | using Utils; 10 | 11 | /// 12 | /// When inheriting IProvide, the class must call this.Provide() somewhere in the setup. 13 | /// This analyzer checks that the class does not forget to call this.Provide(). 14 | /// 15 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 16 | public class AutoInjectProvideAnalyzer : DiagnosticAnalyzer { 17 | public override ImmutableArray SupportedDiagnostics { 18 | get; 19 | } = [Diagnostics.MissingAutoInjectProvideDescriptor]; 20 | 21 | public override void Initialize(AnalysisContext context) { 22 | context.EnableConcurrentExecution(); 23 | 24 | context.ConfigureGeneratedCodeAnalysis( 25 | GeneratedCodeAnalysisFlags.Analyze | 26 | GeneratedCodeAnalysisFlags.ReportDiagnostics 27 | ); 28 | 29 | context.RegisterSyntaxNodeAction( 30 | AnalyzeClassDeclaration, 31 | SyntaxKind.ClassDeclaration 32 | ); 33 | } 34 | 35 | private void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) { 36 | var classDeclaration = (ClassDeclarationSyntax)context.Node; 37 | 38 | // Check that IProvide is implemented by the class, as these are the only classes that need to call Provide(). 39 | var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration, context.CancellationToken); 40 | var implementsIProvide = classSymbol?.AllInterfaces 41 | .Any(i => i.Name == Constants.PROVIDER_INTERFACE_NAME && i.IsGenericType) == true; 42 | 43 | if (!implementsIProvide) { 44 | return; 45 | } 46 | 47 | // Check that Meta attribute has an AutoInject Provider type (ex: [Meta(typeof(IAutoNode))]) 48 | var attributes = classDeclaration.AttributeLists.SelectMany(list => list.Attributes 49 | ).Where(attribute => attribute.Name.ToString() == Constants.META_ATTRIBUTE_NAME 50 | && attribute.ArgumentList?.Arguments.Any(arg => 51 | arg.Expression is TypeOfExpressionSyntax { Type: IdentifierNameSyntax identifierName } && 52 | Constants.ProviderMetaNames.Contains(identifierName.Identifier.ValueText) 53 | ) == true 54 | ) 55 | .ToList(); 56 | 57 | if (attributes.Count == 0) { 58 | return; 59 | } 60 | 61 | const string provideMethodName = "Provide"; 62 | 63 | // Check if the class calls "this.Provide()" anywhere 64 | var hasProvide = classDeclaration 65 | .DescendantNodes() 66 | .OfType() 67 | .Any(invocation => 68 | invocation.Expression is MemberAccessExpressionSyntax { 69 | Name.Identifier.ValueText: provideMethodName, Expression: ThisExpressionSyntax 70 | }); 71 | 72 | if (hasProvide) { 73 | return; 74 | } 75 | 76 | // No provide call found, report the diagnostic 77 | context.ReportDiagnostic( 78 | Diagnostics.MissingAutoInjectProvide( 79 | attributes[0].GetLocation(), 80 | classDeclaration.Identifier.ValueText 81 | ) 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers/src/fixes/AutoInjectNotificationOverrideFixProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Analyzers.Fixes; 2 | 3 | using System.Collections.Immutable; 4 | using System.Composition; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.CodeAnalysis; 9 | using Microsoft.CodeAnalysis.CodeActions; 10 | using Microsoft.CodeAnalysis.CodeFixes; 11 | using Microsoft.CodeAnalysis.CSharp; 12 | using Microsoft.CodeAnalysis.CSharp.Syntax; 13 | using Microsoft.CodeAnalysis.Formatting; 14 | using Microsoft.CodeAnalysis.Simplification; 15 | using Utils; 16 | 17 | [ 18 | ExportCodeFixProvider( 19 | LanguageNames.CSharp, 20 | Name = nameof(AutoInjectNotificationOverrideFixProvider) 21 | ), 22 | Shared 23 | ] 24 | public class AutoInjectNotificationOverrideFixProvider : CodeFixProvider { 25 | public sealed override ImmutableArray FixableDiagnosticIds => 26 | [Diagnostics.MissingAutoInjectNotificationOverrideDescriptor.Id]; 27 | 28 | public sealed override FixAllProvider GetFixAllProvider() => 29 | WellKnownFixAllProviders.BatchFixer; 30 | 31 | public sealed override async Task RegisterCodeFixesAsync( 32 | CodeFixContext context) { 33 | var root = await context.Document 34 | .GetSyntaxRootAsync(context.CancellationToken) 35 | .ConfigureAwait(false); 36 | if (root is null) { 37 | return; 38 | } 39 | 40 | var diagnostic = context.Diagnostics.First(); 41 | var diagnosticSpan = diagnostic.Location.SourceSpan; 42 | 43 | // Find the type declaration identified by the diagnostic 44 | var typeDeclaration = root 45 | .FindToken(diagnosticSpan.Start) 46 | .Parent? 47 | .AncestorsAndSelf() 48 | .OfType() 49 | .FirstOrDefault(); 50 | if (typeDeclaration is null) { 51 | return; 52 | } 53 | 54 | context.RegisterCodeFix( 55 | CodeAction.Create( 56 | title: "Add \"public override void _Notification(int what) => this.Notify(what);\" method", 57 | createChangedDocument: c => 58 | AddAutoInjectNotificationOverrideAsync( 59 | context.Document, 60 | typeDeclaration, 61 | c 62 | ), 63 | equivalenceKey: nameof(AutoInjectNotificationOverrideFixProvider) 64 | ), 65 | diagnostic 66 | ); 67 | } 68 | 69 | private static async Task AddAutoInjectNotificationOverrideAsync( 70 | Document document, 71 | TypeDeclarationSyntax typeDeclaration, 72 | CancellationToken cancellationToken) { 73 | 74 | var methodDeclaration = SyntaxFactory.MethodDeclaration( 75 | SyntaxFactory.PredefinedType( 76 | SyntaxFactory.Token(SyntaxKind.VoidKeyword) 77 | ), 78 | "_Notification" 79 | ) 80 | .WithModifiers( 81 | SyntaxFactory.TokenList( 82 | SyntaxFactory.Token(SyntaxKind.PublicKeyword), 83 | SyntaxFactory.Token(SyntaxKind.OverrideKeyword) 84 | ) 85 | ) 86 | .WithParameterList( 87 | SyntaxFactory.ParameterList( 88 | SyntaxFactory.SingletonSeparatedList( 89 | SyntaxFactory 90 | .Parameter(SyntaxFactory.Identifier("what")) 91 | .WithType( 92 | SyntaxFactory.PredefinedType( 93 | SyntaxFactory.Token(SyntaxKind.IntKeyword) 94 | ) 95 | ) 96 | ) 97 | ) 98 | ); 99 | 100 | var expressionBody = SyntaxFactory 101 | .InvocationExpression( 102 | SyntaxFactory.MemberAccessExpression( 103 | SyntaxKind.SimpleMemberAccessExpression, 104 | SyntaxFactory.ThisExpression(), 105 | SyntaxFactory.IdentifierName("Notify") 106 | ) 107 | ) 108 | .WithArgumentList( 109 | SyntaxFactory.ArgumentList( 110 | SyntaxFactory.SingletonSeparatedList( 111 | SyntaxFactory.Argument(SyntaxFactory.IdentifierName("what")) 112 | ) 113 | ) 114 | ); 115 | 116 | var arrowExpressionClause = SyntaxFactory 117 | .ArrowExpressionClause(expressionBody) 118 | .WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation); 119 | 120 | methodDeclaration = methodDeclaration 121 | .WithExpressionBody(arrowExpressionClause) 122 | .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); 123 | 124 | // Insert the new method at the beginning of the class members 125 | var existingMembers = typeDeclaration.Members; 126 | var newMembers = existingMembers.Insert(0, methodDeclaration); 127 | 128 | // Update the type declaration with the new list of members 129 | var newTypeDeclaration = typeDeclaration.WithMembers(newMembers); 130 | 131 | // Get the current root and replace the type declaration 132 | var root = await document 133 | .GetSyntaxRootAsync(cancellationToken) 134 | .ConfigureAwait(false); 135 | if (root is null) { 136 | return document; 137 | } 138 | 139 | var newRoot = root.ReplaceNode(typeDeclaration, newTypeDeclaration); 140 | 141 | // Return the updated document. 142 | return document.WithSyntaxRoot(newRoot); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers/src/fixes/AutoInjectNotifyMissingFixProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Analyzers.Fixes; 2 | 3 | using System.Collections.Immutable; 4 | using System.Composition; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.CodeAnalysis; 9 | using Microsoft.CodeAnalysis.CodeActions; 10 | using Microsoft.CodeAnalysis.CodeFixes; 11 | using Microsoft.CodeAnalysis.CSharp; 12 | using Microsoft.CodeAnalysis.CSharp.Syntax; 13 | using Microsoft.CodeAnalysis.Formatting; 14 | using Utils; 15 | 16 | [ 17 | ExportCodeFixProvider( 18 | LanguageNames.CSharp, 19 | Name = nameof(AutoInjectNotifyMissingFixProvider) 20 | ), 21 | Shared 22 | ] 23 | public class AutoInjectNotifyMissingFixProvider : CodeFixProvider { 24 | public sealed override ImmutableArray FixableDiagnosticIds => 25 | [Diagnostics.MissingAutoInjectNotifyDescriptor.Id]; 26 | 27 | public sealed override FixAllProvider GetFixAllProvider() => 28 | WellKnownFixAllProviders.BatchFixer; 29 | 30 | public sealed override async Task RegisterCodeFixesAsync( 31 | CodeFixContext context) { 32 | var root = await context.Document 33 | .GetSyntaxRootAsync(context.CancellationToken) 34 | .ConfigureAwait(false); 35 | if (root is null) { 36 | return; 37 | } 38 | 39 | var diagnostic = context.Diagnostics.First(); 40 | var diagnosticSpan = diagnostic.Location.SourceSpan; 41 | 42 | // Find the type declaration identified by the diagnostic 43 | var typeDeclaration = root 44 | .FindToken(diagnosticSpan.Start) 45 | .Parent? 46 | .AncestorsAndSelf() 47 | .OfType().FirstOrDefault(); 48 | if (typeDeclaration is null) { 49 | return; 50 | } 51 | 52 | context.RegisterCodeFix( 53 | CodeAction.Create( 54 | title: "Add \"this.Notify(what);\" to existing \"_Notification\" override", 55 | createChangedDocument: c => 56 | AddAutoInjectNotifyCallAsync(context.Document, typeDeclaration, c), 57 | equivalenceKey: nameof(AutoInjectNotificationOverrideFixProvider) 58 | ), 59 | diagnostic 60 | ); 61 | } 62 | 63 | private static async Task AddAutoInjectNotifyCallAsync( 64 | Document document, 65 | TypeDeclarationSyntax typeDeclaration, 66 | CancellationToken cancellationToken) { 67 | const string methodNameToFind = "_Notification"; 68 | 69 | // Find the method with the specified name and a single parameter of type int 70 | var methodAndParameter = typeDeclaration.Members 71 | .OfType() 72 | .Where( 73 | m => 74 | m.Identifier.ValueText == methodNameToFind 75 | && m.ParameterList.Parameters.Count == 1 76 | ) 77 | .Select( 78 | m => new { 79 | Method = m, 80 | Parameter = m 81 | .ParameterList 82 | .Parameters 83 | .FirstOrDefault( 84 | p => 85 | p.Type is PredefinedTypeSyntax pts 86 | && pts.Keyword.IsKind(SyntaxKind.IntKeyword) 87 | ) 88 | } 89 | ) 90 | .FirstOrDefault(); 91 | 92 | var originalMethodNode = methodAndParameter?.Method; 93 | var parameterSyntax = methodAndParameter?.Parameter; 94 | 95 | if (originalMethodNode is null || parameterSyntax is null) { 96 | // Expected method not found or parameter is missing 97 | return document; 98 | } 99 | 100 | // Get the actual name of the parameter from the found method 101 | // It really should be "what", but this makes it more robust 102 | // to changes in the parameter name 103 | var actualParameterName = parameterSyntax.Identifier.ValueText; 104 | 105 | // Construct the statement to add 106 | const string methodToInvokeName = "Notify"; 107 | 108 | var statementToAdd = SyntaxFactory.ExpressionStatement( 109 | SyntaxFactory.InvocationExpression( 110 | SyntaxFactory.MemberAccessExpression( 111 | SyntaxKind.SimpleMemberAccessExpression, 112 | SyntaxFactory.ThisExpression(), 113 | SyntaxFactory.IdentifierName(methodToInvokeName) 114 | ) 115 | ) 116 | .WithArgumentList( 117 | SyntaxFactory.ArgumentList( 118 | SyntaxFactory.SingletonSeparatedList( 119 | SyntaxFactory.Argument( 120 | SyntaxFactory.IdentifierName(actualParameterName) 121 | ) 122 | ) 123 | ) 124 | ) 125 | ) 126 | .WithAdditionalAnnotations(Formatter.Annotation); 127 | 128 | // Add the statement to the method body 129 | return await MethodModifier.AddStatementToMethodBodyAsync( 130 | document, 131 | typeDeclaration, 132 | originalMethodNode, 133 | statementToAdd, 134 | cancellationToken 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers/src/fixes/AutoInjectProvideFixProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Analyzers.Fixes; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Collections.Immutable; 6 | using System.Composition; 7 | using System.Linq; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Microsoft.CodeAnalysis; 11 | using Microsoft.CodeAnalysis.CodeActions; 12 | using Microsoft.CodeAnalysis.CodeFixes; 13 | using Microsoft.CodeAnalysis.CSharp; 14 | using Microsoft.CodeAnalysis.CSharp.Syntax; 15 | using Microsoft.CodeAnalysis.Formatting; 16 | using Microsoft.CodeAnalysis.Simplification; 17 | using Utils; 18 | 19 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AutoInjectProvideFixProvider))] 20 | [Shared] 21 | public class AutoInjectProvideFixProvider : CodeFixProvider { 22 | public const string SETUP_METHOD_NAME = "Setup"; 23 | public const string ONREADY_METHOD_NAME = "OnReady"; 24 | public const string READY_OVERRIDE_METHOD_NAME = "_Ready"; 25 | 26 | public sealed override ImmutableArray FixableDiagnosticIds => 27 | [Diagnostics.MissingAutoInjectProvideDescriptor.Id]; 28 | 29 | public sealed override FixAllProvider GetFixAllProvider() => 30 | WellKnownFixAllProviders.BatchFixer; 31 | 32 | public sealed override async Task RegisterCodeFixesAsync( 33 | CodeFixContext context) { 34 | var root = await context.Document 35 | .GetSyntaxRootAsync(context.CancellationToken) 36 | .ConfigureAwait(false); 37 | if (root is null) { 38 | return; 39 | } 40 | 41 | var diagnostic = context.Diagnostics.First(); 42 | var diagnosticSpan = diagnostic.Location.SourceSpan; 43 | 44 | // Find the type declaration identified by the diagnostic. 45 | var typeDeclaration = root 46 | .FindToken(diagnosticSpan.Start) 47 | .Parent? 48 | .AncestorsAndSelf() 49 | .OfType() 50 | .FirstOrDefault(); 51 | if (typeDeclaration is null) { 52 | return; 53 | } 54 | 55 | // Register code fixes for either creating or modifying methods that 56 | // call `this.Provide()`. 57 | 58 | // Setup() Method Fixes 59 | RegisterMethodFixesAsync( 60 | context, typeDeclaration, diagnostic, 61 | SETUP_METHOD_NAME, 62 | m => 63 | m.Identifier.Text == SETUP_METHOD_NAME 64 | && m.Modifiers.Any(SyntaxKind.PublicKeyword), 65 | SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) 66 | ); 67 | 68 | // OnReady() Method Fixes 69 | RegisterMethodFixesAsync( 70 | context, typeDeclaration, diagnostic, 71 | ONREADY_METHOD_NAME, 72 | m => 73 | m.Identifier.Text == ONREADY_METHOD_NAME 74 | && m.Modifiers.Any(SyntaxKind.PublicKeyword), 75 | SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) 76 | ); 77 | 78 | // _Ready() Method Fixes 79 | RegisterMethodFixesAsync( 80 | context, typeDeclaration, diagnostic, 81 | READY_OVERRIDE_METHOD_NAME, 82 | m => 83 | m.Identifier.Text == READY_OVERRIDE_METHOD_NAME 84 | && m.Modifiers.Any(SyntaxKind.PublicKeyword) 85 | && m.Modifiers.Any(SyntaxKind.OverrideKeyword), 86 | SyntaxFactory.TokenList( 87 | SyntaxFactory.Token(SyntaxKind.PublicKeyword), 88 | SyntaxFactory.Token(SyntaxKind.OverrideKeyword) 89 | ) 90 | ); 91 | } 92 | 93 | public static string GetCodeFixEquivalenceKey( 94 | string methodName, 95 | bool methodExists) { 96 | var operation = methodExists ? "CreateNew" : "AddCallTo"; 97 | return $"{nameof(AutoInjectProvideFixProvider)}_{operation}_{methodName}"; 98 | } 99 | 100 | /// 101 | /// Registers code fixes for a method that either adds a call to an existing 102 | /// method if it exists or creates a new method with the call. 103 | /// 104 | /// Code fix context 105 | /// Type declaration of parent class 106 | /// Diagnostic that triggered this 107 | /// Method name to create 108 | /// Predicate to find existing method 109 | /// 110 | /// Method modifiers to add to created method 111 | /// 112 | private static void RegisterMethodFixesAsync( 113 | CodeFixContext context, 114 | TypeDeclarationSyntax typeDeclaration, 115 | Diagnostic diagnostic, 116 | string methodName, 117 | Func findPredicate, 118 | IEnumerable creationModifiers 119 | ) { 120 | var existingMethod = typeDeclaration.Members 121 | .OfType() 122 | .FirstOrDefault(findPredicate); 123 | 124 | if (existingMethod is not null) { 125 | // Method exists, offer to add a call to it 126 | context.RegisterCodeFix( 127 | CodeAction.Create( 128 | title: 129 | $"Add \"this.Provide();\" to existing \"{methodName}()\" method", 130 | createChangedDocument: c => 131 | MethodModifier.AddCallToMethod( 132 | context.Document, 133 | typeDeclaration, 134 | existingMethod, 135 | "Provide", 136 | c 137 | ), 138 | equivalenceKey: GetCodeFixEquivalenceKey(methodName, true) 139 | ), 140 | diagnostic 141 | ); 142 | } 143 | else { 144 | // Method does not exist, offer to create it 145 | context.RegisterCodeFix( 146 | CodeAction.Create( 147 | title: 148 | $"Create \"{methodName}()\" method that calls \"this.Provide()\"", 149 | createChangedDocument: c => 150 | AddNewMethodAsync( 151 | context.Document, 152 | typeDeclaration, 153 | methodName, 154 | creationModifiers, 155 | c 156 | ), 157 | equivalenceKey: GetCodeFixEquivalenceKey(methodName, false) 158 | ), 159 | diagnostic 160 | ); 161 | } 162 | } 163 | 164 | private static async Task AddNewMethodAsync( 165 | Document document, 166 | TypeDeclarationSyntax typeDeclaration, 167 | string identifier, 168 | IEnumerable creationModifiers, 169 | CancellationToken cancellationToken) { 170 | // Create the new method 171 | var mewMethod = SyntaxFactory 172 | .MethodDeclaration( 173 | SyntaxFactory.PredefinedType( 174 | SyntaxFactory.Token(SyntaxKind.VoidKeyword) 175 | ), 176 | identifier 177 | ) 178 | .WithModifiers(SyntaxFactory.TokenList(creationModifiers)) 179 | .WithBody( 180 | SyntaxFactory.Block( 181 | SyntaxFactory.SingletonList( 182 | SyntaxFactory.ParseStatement( 183 | Constants.PROVIDE_NEW_METHOD_BODY) 184 | .WithAdditionalAnnotations( 185 | Formatter.Annotation, 186 | Simplifier.Annotation 187 | ) 188 | ) 189 | ) 190 | ); 191 | 192 | // Add the new method to the class 193 | var newTypeDeclaration = typeDeclaration.AddMembers(mewMethod); 194 | // Replace the old type declaration with the new one 195 | var root = await document.GetSyntaxRootAsync(cancellationToken); 196 | if (root is null) { 197 | return document; 198 | } 199 | 200 | var newRoot = root.ReplaceNode(typeDeclaration, newTypeDeclaration); 201 | // Return the updated document 202 | return document.WithSyntaxRoot(newRoot); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers/src/utils/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Analyzers.Utils; 2 | 3 | public static class Constants { 4 | public const string META_ATTRIBUTE_NAME = "Meta"; 5 | 6 | /// 7 | /// Type names that we look for in Meta attributes to determine if a class needs a this.Provide() call. 8 | /// 9 | public static readonly string[] ProviderMetaNames = [ 10 | "IAutoNode", 11 | "IProvider", 12 | ]; 13 | 14 | public const string PROVIDER_INTERFACE_NAME = "IProvide"; 15 | 16 | /// 17 | /// Type names that we look for in Meta attributes to determine if a class needs a this.Notify(what) call. 18 | /// 19 | public static readonly string[] AutoInjectTypeNames = [ 20 | "IAutoNode", 21 | "IAutoOn", 22 | "IAutoConnect", 23 | "IAutoInit", 24 | "IProvider", 25 | "IDependent", 26 | ]; 27 | 28 | public const string PROVIDE_METHOD_NAME = "this.Provide()"; 29 | public const string PROVIDE_NEW_METHOD_BODY = """ 30 | // Call the this.Provide() method once your dependencies have been initialized. 31 | this.Provide(); 32 | """; 33 | public const string NOTIFY_METHOD_NAME = "this.Notify(what)"; 34 | } 35 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers/src/utils/Diagnostics.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Analyzers.Utils; 2 | 3 | using Microsoft.CodeAnalysis; 4 | 5 | public static class Diagnostics { 6 | private const string ERR_PREFIX = "AUTO_INJECT"; 7 | private const string ERR_CATEGORY = "Chickensoft.AutoInject.Analyzers"; 8 | 9 | public static DiagnosticDescriptor MissingAutoInjectNotificationOverrideDescriptor { get; } = new( 10 | id: $"{ERR_PREFIX}001", 11 | title: $"Missing \"_Notification\" method override", 12 | messageFormat: $"Missing override of \"_Notification\" in AutoInject class implementation `{{0}}`", 13 | category: ERR_CATEGORY, 14 | defaultSeverity: DiagnosticSeverity.Error, 15 | isEnabledByDefault: true, 16 | description: "Overriding the _Notification method is required to pass the lifecycle of the Godot node to AutoInject. Without this, all AutoInject functionality will not work as expected." 17 | ); 18 | 19 | public static Diagnostic MissingAutoInjectNotificationOverride( 20 | Location location, string name 21 | ) => Diagnostic.Create(MissingAutoInjectNotificationOverrideDescriptor, location, name); 22 | 23 | public static DiagnosticDescriptor MissingAutoInjectNotifyDescriptor { get; } = new( 24 | id: $"{ERR_PREFIX}002", 25 | title: $"Missing \"{Constants.NOTIFY_METHOD_NAME}\" method call", 26 | messageFormat: $"Missing \"{Constants.NOTIFY_METHOD_NAME}\" in AutoInject class implementation `{{0}}`", 27 | category: ERR_CATEGORY, 28 | defaultSeverity: DiagnosticSeverity.Error, 29 | isEnabledByDefault: true, 30 | description: "Calling this.Notify(what); within the _Notification method is required to pass the lifecycle of the Godot node to AutoInject. Without this, all AutoInject functionality will not work as expected." 31 | ); 32 | 33 | public static Diagnostic MissingAutoInjectNotify( 34 | Location location, string name 35 | ) => Diagnostic.Create(MissingAutoInjectNotifyDescriptor, location, name); 36 | 37 | public static DiagnosticDescriptor MissingAutoInjectProvideDescriptor { get; } = new( 38 | id: $"{ERR_PREFIX}003", 39 | title: $"Missing \"{Constants.PROVIDE_METHOD_NAME}\" call in provider class", 40 | messageFormat: $"Missing \"{Constants.PROVIDE_METHOD_NAME}\" call in provider class implementation `{{0}}`", 41 | category: ERR_CATEGORY, 42 | defaultSeverity: DiagnosticSeverity.Error, 43 | isEnabledByDefault: true, 44 | description: "Calling the Provide method is required to provide dependencies to the AutoInject system. Without this, the provided dependencies will not be injected and dependent classes will not function as expected." 45 | ); 46 | 47 | public static Diagnostic MissingAutoInjectProvide( 48 | Location location, string name 49 | ) => Diagnostic.Create(MissingAutoInjectProvideDescriptor, location, name); 50 | } 51 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Analyzers/src/utils/MethodModifier.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Analyzers.Utils; 2 | 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; 8 | using Microsoft.CodeAnalysis.Formatting; 9 | 10 | public static class MethodModifier { 11 | /// 12 | /// Adds a this.method call to the end of a specified method within a type declaration. 13 | /// 14 | /// Document to modify. 15 | /// Type declaration off the class. 16 | /// The method to add the call to. 17 | /// Name of the method to call at the end of the target method. 18 | /// Cancellation Token 19 | /// Modified document 20 | public static async Task AddCallToMethod(Document document, 21 | TypeDeclarationSyntax typeDeclaration, 22 | MethodDeclarationSyntax originalMethodNode, 23 | string methodToCallName, 24 | CancellationToken cancellationToken) 25 | { 26 | // Construct the parameterless call statement 27 | var parameterlessCallStatement = SyntaxFactory.ExpressionStatement( 28 | SyntaxFactory.InvocationExpression( 29 | SyntaxFactory.MemberAccessExpression( 30 | SyntaxKind.SimpleMemberAccessExpression, 31 | SyntaxFactory.ThisExpression(), 32 | SyntaxFactory.IdentifierName(methodToCallName))) 33 | ).WithAdditionalAnnotations(Formatter.Annotation); 34 | 35 | // Delegate to the more general helper 36 | return await AddStatementToMethodBodyAsync( 37 | document, 38 | typeDeclaration, 39 | originalMethodNode, 40 | parameterlessCallStatement, 41 | cancellationToken 42 | ); 43 | } 44 | 45 | /// 46 | /// Adds a provided statement to the end of a specified method's body within a type declaration. 47 | /// Handles conversion from expression body to block body. 48 | /// 49 | /// Document to modify. 50 | /// Type declaration of the class. 51 | /// The method to add the statement to. 52 | /// The pre-constructed statement to add. 53 | /// Cancellation Token. 54 | /// Modified document. 55 | public static async Task AddStatementToMethodBodyAsync( 56 | Document document, 57 | TypeDeclarationSyntax typeDeclaration, 58 | MethodDeclarationSyntax originalMethodNode, 59 | StatementSyntax statementToAdd, 60 | CancellationToken cancellationToken) 61 | { 62 | var methodInProgress = originalMethodNode; 63 | BlockSyntax finalNewBody; 64 | 65 | if (originalMethodNode.Body is not null) // Existing block body 66 | { 67 | var existingStatements = originalMethodNode.Body.Statements; 68 | var updatedStatements = existingStatements.Add(statementToAdd); 69 | finalNewBody = originalMethodNode.Body.WithStatements(updatedStatements) 70 | .WithAdditionalAnnotations(Formatter.Annotation); 71 | } 72 | else // Expression body or missing body 73 | { 74 | var statementsForNewBlock = SyntaxFactory.List(); 75 | if (originalMethodNode.ExpressionBody is not null) 76 | { 77 | var originalExpression = originalMethodNode.ExpressionBody.Expression; 78 | var statementFromExpr = SyntaxFactory.ExpressionStatement(originalExpression); 79 | 80 | // Make sure to preserve the trailing trivia from the original method's semicolon token 81 | // If we don't do this we will lose any code comments or whitespace that was after the semicolon 82 | var originalMethodSemicolon = originalMethodNode.SemicolonToken; 83 | if (!originalMethodSemicolon.IsKind(SyntaxKind.None) && !originalMethodSemicolon.IsMissing) 84 | { 85 | var originalSemicolonTrailingTrivia = originalMethodSemicolon.TrailingTrivia; 86 | if (originalSemicolonTrailingTrivia.Any()) 87 | { 88 | statementFromExpr = statementFromExpr.WithSemicolonToken( 89 | statementFromExpr.SemicolonToken.WithTrailingTrivia(originalSemicolonTrailingTrivia) 90 | ); 91 | } 92 | } 93 | statementFromExpr = statementFromExpr.WithAdditionalAnnotations(Formatter.Annotation); 94 | statementsForNewBlock = statementsForNewBlock.Add(statementFromExpr); 95 | 96 | // Remove the old expression body from the method 97 | methodInProgress = methodInProgress 98 | .WithExpressionBody(null) 99 | .WithSemicolonToken(SyntaxFactory.MissingToken(SyntaxKind.SemicolonToken)); 100 | } 101 | 102 | // Add the provided statement 103 | statementsForNewBlock = statementsForNewBlock.Add(statementToAdd); 104 | // Create a new block with the collected statements 105 | finalNewBody = SyntaxFactory.Block(statementsForNewBlock) 106 | .WithAdditionalAnnotations(Formatter.Annotation); 107 | } 108 | 109 | // Ensure the method (methodInProgress) ends with a new line 110 | // This is important when converting from an expression body to a block body 111 | var trailingTrivia = methodInProgress.GetTrailingTrivia(); 112 | if (!trailingTrivia.Any() || !trailingTrivia.Last().IsKind(SyntaxKind.EndOfLineTrivia)) 113 | { 114 | // Get the new line character from the document options 115 | // This makes sure we use the same new line character as the rest of the document 116 | var documentOptions = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false); 117 | var newLineCharacter = documentOptions.GetOption(FormattingOptions.NewLine) ?? "\n"; 118 | var newLineTrivia = SyntaxFactory.EndOfLine(newLineCharacter); 119 | 120 | // Add the newline 121 | methodInProgress = methodInProgress.WithTrailingTrivia(trailingTrivia.Add(newLineTrivia)); 122 | } 123 | 124 | var fullyModifiedMethod = methodInProgress.WithBody(finalNewBody); 125 | 126 | // Replace the original method with the modified one in the type declaration 127 | var newTypeDeclaration = typeDeclaration.ReplaceNode(originalMethodNode, fullyModifiedMethod); 128 | 129 | var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); 130 | if (root is null) 131 | { 132 | return document; 133 | } 134 | 135 | var newRoot = root.ReplaceNode(typeDeclaration, newTypeDeclaration); 136 | return document.WithSyntaxRoot(newRoot); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/Chickensoft.AutoInject.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | true 5 | preview 6 | enable 7 | Chickensoft.AutoInject.Tests 8 | 9 | 10 | true 11 | 12 | true 13 | .generated 14 | full 15 | true 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/Chickensoft.AutoInject.Tests.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 2012 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chickensoft.AutoInject.Tests", "Chickensoft.AutoInject.Tests.csproj", "{A8C945CE-3945-4C8C-B388-FCE59FDD5345}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | ExportDebug|Any CPU = ExportDebug|Any CPU 9 | ExportRelease|Any CPU = ExportRelease|Any CPU 10 | EndGlobalSection 11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 12 | {A8C945CE-3945-4C8C-B388-FCE59FDD5345}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {A8C945CE-3945-4C8C-B388-FCE59FDD5345}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {A8C945CE-3945-4C8C-B388-FCE59FDD5345}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU 15 | {A8C945CE-3945-4C8C-B388-FCE59FDD5345}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU 16 | {A8C945CE-3945-4C8C-B388-FCE59FDD5345}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU 17 | {A8C945CE-3945-4C8C-B388-FCE59FDD5345}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU 18 | EndGlobalSection 19 | EndGlobal 20 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/badges/.gdignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chickensoft-games/AutoInject/9aeb73934a2473c13659d9baafb5adb65b07d688/Chickensoft.AutoInject.Tests/badges/.gdignore -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/badges/branch_coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | 48 | Code coverage 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | Generated by: ReportGenerator 5.1.26.0 98 | 99 | 100 | 101 | Coverage 102 | Coverage 103 | 104 | 100%100% 105 | 106 | 107 | 108 | 109 | 110 | Branch coverage 111 | 112 | 113 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/badges/line_coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | 48 | Code coverage 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | Generated by: ReportGenerator 5.1.26.0 98 | 99 | 100 | 101 | Coverage 102 | Coverage 103 | 100%100% 104 | 105 | 106 | 107 | 108 | 109 | Line coverage 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # To collect code coverage, you will need the following environment setup: 4 | # 5 | # - A "GODOT" environment variable pointing to the Godot executable 6 | # - ReportGenerator installed 7 | # 8 | # dotnet tool install -g dotnet-reportgenerator-globaltool 9 | # 10 | # - A version of coverlet > 3.2.0. 11 | # 12 | # As of Jan 2023, this is not yet released. 13 | # 14 | # The included `nuget.config` file will allow you to install a nightly 15 | # version of coverlet from the coverlet nightly nuget feed. 16 | # 17 | # dotnet tool install --global coverlet.console --prerelease. 18 | # 19 | # You can build coverlet yourself, but you will need to edit the path to 20 | # coverlet below to point to your local build of the coverlet dll. 21 | # 22 | # If you need help with coverage, feel free to join the Chickensoft Discord. 23 | # https://chickensoft.games 24 | 25 | dotnet build --no-restore 26 | 27 | coverlet \ 28 | "./.godot/mono/temp/bin/Debug" --verbosity detailed \ 29 | --target "$GODOT" \ 30 | --targetargs "--headless --run-tests --coverage --quit-on-finish" \ 31 | --format "opencover" \ 32 | --output "./coverage/coverage.xml" \ 33 | --exclude-by-file "**/test/**/*.cs" \ 34 | --exclude-by-file "**/*Microsoft.NET.Test.Sdk.Program.cs" \ 35 | --exclude-by-file "**/Godot.SourceGenerators/**/*.cs" \ 36 | --exclude-assemblies-without-sources "missingall" \ 37 | --skipautoprops 38 | 39 | # Projects included via will be collected in code coverage. 40 | # If you want to exclude them, replace the string below with the names of 41 | # the assemblies to ignore. e.g., 42 | # ASSEMBLIES_TO_REMOVE="-AssemblyToRemove1;-AssemblyToRemove2" 43 | ASSEMBLIES_TO_REMOVE="" # "-Chickensoft.AutoInject.Tests" 44 | 45 | reportgenerator \ 46 | -reports:"./coverage/coverage.xml" \ 47 | -targetdir:"./coverage/report" \ 48 | "-assemblyfilters:$ASSEMBLIES_TO_REMOVE" \ 49 | "-classfilters:-TypeRegistry;-GodotPlugins.Game.Main;-Chickensoft.AutoInject.Tests.*;-Chickensoft.AutoInject.IDependent;-Chickensoft.AutoInject.Dependent;-Chickensoft.AutoInject.IProvider;-Chickensoft.AutoInject.Provider" \ 50 | -reporttypes:"Html;Badges" 51 | 52 | # Copy badges into their own folder. The badges folder should be included in 53 | # source control so that the README.md in the root can reference the badges. 54 | 55 | mkdir -p ./badges 56 | mv ./coverage/report/badge_branchcoverage.svg ./badges/branch_coverage.svg 57 | mv ./coverage/report/badge_linecoverage.svg ./badges/line_coverage.svg 58 | 59 | # Determine OS, open coverage accordingly. 60 | 61 | case "$(uname -s)" in 62 | 63 | Darwin) 64 | echo 'Mac OS X' 65 | open coverage/report/index.htm 66 | ;; 67 | 68 | Linux) 69 | echo 'Linux' 70 | open coverage/report/index.htm 71 | ;; 72 | 73 | CYGWIN*|MINGW32*|MSYS*|MINGW*) 74 | echo 'MS Windows' 75 | start coverage/report/index.htm 76 | ;; 77 | 78 | *) 79 | echo 'Other OS' 80 | ;; 81 | esac 82 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/coverage/.gdignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chickensoft-games/AutoInject/9aeb73934a2473c13659d9baafb5adb65b07d688/Chickensoft.AutoInject.Tests/coverage/.gdignore -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://da2tcc2mhkfgi" 6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="Chickensoft.AutoInject.Tests" 14 | run/main_scene="res://test/Tests.tscn" 15 | config/features=PackedStringArray("4.0", "C#", "Mobile") 16 | config/icon="res://icon.svg" 17 | 18 | [dotnet] 19 | 20 | project/assembly_name="Chickensoft.AutoInject.Tests" 21 | 22 | [rendering] 23 | 24 | renderer/rendering_method="mobile" 25 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/auto_connect/AutoConnectExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject; 2 | 3 | using System; 4 | using Chickensoft.GodotNodeInterfaces; 5 | #pragma warning disable CS8019, IDE0005 6 | using Chickensoft.AutoInject; 7 | using Godot; 8 | using System.Collections.Generic; 9 | 10 | public static class AutoConnectExtensions { 11 | /// 12 | /// Initialize the fake node tree for unit testing. 13 | /// 14 | /// Godot node. 15 | /// Map of node paths to mock nodes. 16 | /// 17 | public static void FakeNodeTree( 18 | this Node node, Dictionary? nodes 19 | ) { 20 | if (node is not IAutoConnect autoConnect) { 21 | throw new InvalidOperationException( 22 | "Cannot create a fake node tree on a node without the AutoConnect " + 23 | "mixin." 24 | ); 25 | } 26 | 27 | autoConnect.FakeNodes = new(node, nodes); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/auto_connect/AutoConnector.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject; 2 | 3 | using System; 4 | using System.Runtime.CompilerServices; 5 | using Chickensoft.GodotNodeInterfaces; 6 | #pragma warning disable CS8019, IDE0005 7 | using Chickensoft.AutoInject; 8 | using Godot; 9 | using Chickensoft.Introspection; 10 | using System.Collections.Generic; 11 | 12 | public static class AutoConnector { 13 | public class TypeChecker : ITypeReceiver { 14 | public object Value { get; set; } = default!; 15 | 16 | public bool Result { get; private set; } 17 | 18 | public void Receive() => Result = Value is T; 19 | } 20 | 21 | private static readonly TypeChecker _checker = new(); 22 | 23 | public static void ConnectNodes( 24 | IEnumerable properties, 25 | IAutoConnect autoConnect 26 | ) { 27 | var node = (Node)autoConnect; 28 | foreach (var property in properties) { 29 | if ( 30 | !property.Attributes.TryGetValue( 31 | typeof(NodeAttribute), out var nodeAttributes 32 | ) 33 | ) { 34 | continue; 35 | } 36 | var nodeAttribute = (NodeAttribute)nodeAttributes[0]; 37 | 38 | var path = nodeAttribute.Path ?? AsciiToPascalCase(property.Name); 39 | 40 | Exception? e; 41 | 42 | // First, check to see if the node has been faked for testing. 43 | // Faked nodes take precedence over real nodes. 44 | // 45 | // FakeNodes will never be null on an AutoConnect node, actually. 46 | if (autoConnect.FakeNodes!.GetNode(path) is { } fakeNode) { 47 | // We found a faked node for this path. Make sure it's the expected 48 | // type. 49 | _checker.Value = fakeNode; 50 | 51 | property.TypeNode.GenericTypeGetter(_checker); 52 | 53 | var satisfiesFakeType = _checker.Result; 54 | 55 | if (!satisfiesFakeType) { 56 | e = new InvalidOperationException( 57 | $"Found a faked node at '{path}' of type " + 58 | $"'{fakeNode.GetType().Name}' that is not the expected type " + 59 | $"'{property.TypeNode.ClosedType}' for member " + 60 | $"'{property.Name}' on '{node.Name}'." 61 | ); 62 | GD.PushError(e.Message); 63 | throw e; 64 | } 65 | // Faked node satisfies the expected type :) 66 | if (property.Setter is { } setter) { 67 | setter(node, fakeNode); 68 | } 69 | 70 | continue; 71 | } 72 | 73 | // We're dealing with what should be an actual node in the tree. 74 | var potentialChild = node.GetNodeOrNull(path); 75 | 76 | if (potentialChild is not Node child) { 77 | e = new InvalidOperationException( 78 | $"AutoConnect: Node at '{path}' does not exist in either the real " + 79 | $"or fake subtree for '{node.Name}' member '{property.Name}' of " + 80 | $"type '{property.TypeNode.ClosedType}'." 81 | ); 82 | GD.PushError(e.Message); 83 | throw e; 84 | } 85 | 86 | // see if the unchecked node satisfies the expected type of node from the 87 | // property type 88 | _checker.Value = child; 89 | property.TypeNode.GenericTypeGetter(_checker); 90 | var originalNodeSatisfiesType = _checker.Result; 91 | 92 | if (originalNodeSatisfiesType) { 93 | // Property expected a vanilla Godot node type and it matched, so we 94 | // set it and leave. 95 | if (property.Setter is { } setter) { 96 | setter(node, child); 97 | } 98 | continue; 99 | } 100 | 101 | // Plain Godot node type wasn't expected, so we need to check if the 102 | // property was expecting a Godot node interface type. 103 | // 104 | // Check to see if the node needs to be adapted to satisfy an 105 | // expected interface type. 106 | var adaptedChild = GodotInterfaces.AdaptNode(child); 107 | _checker.Value = adaptedChild; 108 | 109 | property.TypeNode.GenericTypeGetter(_checker); 110 | var adaptedChildSatisfiesType = _checker.Result; 111 | 112 | if (adaptedChildSatisfiesType) { 113 | if (property.Setter is { } setter) { 114 | setter(node, adaptedChild); 115 | } 116 | continue; 117 | } 118 | 119 | // Tell user we can't connect the node to the property. 120 | e = new InvalidOperationException( 121 | $"Node at '{path}' of type '{child.GetType().Name}' does not " + 122 | $"satisfy the expected type '{property.TypeNode.ClosedType}' for " + 123 | $"member '{property.Name}' on '{node.Name}'." 124 | ); 125 | GD.PushError(e.Message); 126 | throw e; 127 | } 128 | } 129 | 130 | /// 131 | /// 132 | /// Converts an ASCII string to PascalCase. This looks insane, but it is the 133 | /// fastest out of all the benchmarks I did. 134 | /// 135 | /// 136 | /// Since messing with strings can be slow and looking up nodes is a common 137 | /// operation, this is a good place to optimize. No heap allocations! 138 | /// 139 | /// 140 | /// Removes underscores, always capitalizes the first letter, and capitalizes 141 | /// the first letter after an underscore. 142 | /// 143 | /// 144 | /// Input string. 145 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 146 | public static string AsciiToPascalCase(string input) { 147 | var span = input.AsSpan(); 148 | Span output = stackalloc char[span.Length + 1]; 149 | var outputIndex = 1; 150 | 151 | output[0] = '%'; 152 | 153 | for (var i = 1; i < span.Length + 1; i++) { 154 | var c = span[i - 1]; 155 | 156 | if (c == '_') { continue; } 157 | 158 | output[outputIndex++] = i == 1 || span[i - 2] == '_' 159 | ? (char)(c & 0xDF) 160 | : c; 161 | } 162 | 163 | return new string(output[..outputIndex]); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/auto_connect/IAutoConnect.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject; 2 | 3 | using System.Runtime.CompilerServices; 4 | using Chickensoft.GodotNodeInterfaces; 5 | #pragma warning disable CS8019, IDE0005 6 | using Chickensoft.AutoInject; 7 | using Godot; 8 | using Chickensoft.Introspection; 9 | using System.Collections.Generic; 10 | 11 | /// 12 | /// Apply this mixin to your introspective node to automatically connect 13 | /// declared node references to their corresponding instances in the scene tree. 14 | /// 15 | [Mixin] 16 | public interface IAutoConnect : IMixin, IFakeNodeTreeEnabled { 17 | 18 | FakeNodeTree? IFakeNodeTreeEnabled.FakeNodes { 19 | get { 20 | _AddStateIfNeeded(); 21 | return MixinState.Get(); 22 | } 23 | set { 24 | if (value is { } tree) { 25 | MixinState.Overwrite(value); 26 | } 27 | } 28 | } 29 | 30 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 31 | void IMixin.Handler() { 32 | var what = MixinState.Get().Notification; 33 | 34 | if (what == Node.NotificationEnterTree) { 35 | AutoConnector.ConnectNodes(Types.Graph.GetProperties(GetType()), this); 36 | } 37 | } 38 | 39 | #pragma warning disable IDE1006 // Naming Styles 40 | public void _AddStateIfNeeded(Dictionary? nodes = null) { 41 | if (this is not Node node) { return; } 42 | if (!MixinState.Has()) { 43 | MixinState.Overwrite(new FakeNodeTree(node, nodes)); 44 | } 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/auto_connect/NodeAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject; 2 | 3 | using System; 4 | #pragma warning disable CS8019, IDE0005 5 | using Chickensoft.AutoInject; 6 | 7 | /// 8 | /// Node attribute. Apply this to properties or fields that need to be 9 | /// automatically connected to a corresponding node instance in the scene tree. 10 | /// 11 | /// Godot node path. If not provided, the name of the 12 | /// property will be converted to PascalCase (with any leading 13 | /// underscores removed) and used as a unique node identifier 14 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] 15 | public class NodeAttribute(string? path = null) : Attribute { 16 | /// 17 | /// Explicit node path or unique identifier that the tagged property or field 18 | /// should reference. If not provided (or null), the name of the property or 19 | /// field itself will be converted to PascalCase (with any leading 20 | /// underscores removed) and used as a unique node identifier. For example, 21 | /// the reference `Node2D _myNode` would be connected to `%MyNode`. 22 | /// 23 | public string? Path { get; } = path; 24 | } 25 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/auto_init/IAutoInit.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject; 2 | 3 | using Chickensoft.Introspection; 4 | #pragma warning disable IDE0005 5 | using Chickensoft.AutoInject; 6 | using Godot; 7 | 8 | /// 9 | /// Mixin which invokes an Initialize method just before Ready is received. 10 | /// The initialize method is provided as a convenient place to initialize 11 | /// non-node related values that may be needed by the node's Ready method. 12 | ///
13 | /// Distinguishing between initialization and _Ready helps make unit testing 14 | /// nodes easier. 15 | ///
16 | [Mixin] 17 | public partial interface IAutoInit : IMixin { 18 | private sealed class AutoInitState { 19 | public bool IsTesting { get; set; } 20 | } 21 | 22 | /// 23 | /// True if the node is being unit-tested. When unit-tested, setup callbacks 24 | /// will not be invoked. 25 | /// 26 | public bool IsTesting { 27 | get { 28 | CreateStateIfNeeded(); 29 | return MixinState.Get().IsTesting; 30 | } 31 | set { 32 | CreateStateIfNeeded(); 33 | MixinState.Get().IsTesting = value; 34 | } 35 | } 36 | 37 | void IMixin.Handler() { 38 | if (this is not Node node) { 39 | return; 40 | } 41 | 42 | node.__SetupNotificationStateIfNeeded(); 43 | 44 | var what = MixinState.Get().Notification; 45 | 46 | if (what == Node.NotificationReady && !IsTesting) { 47 | // Call initialize before _Ready if we're not testing. 48 | Initialize(); 49 | } 50 | } 51 | 52 | private void CreateStateIfNeeded() { 53 | if (MixinState.Has()) { return; } 54 | 55 | MixinState.Overwrite(new AutoInitState()); 56 | } 57 | 58 | /// 59 | /// Initialization method invoked before Ready — perform any non-node 60 | /// related setup and initialization here. 61 | /// 62 | void Initialize() { } 63 | } 64 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/auto_inject/dependent/DependencyAttribute.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable 2 | namespace Chickensoft.AutoInject; 3 | 4 | using System; 5 | 6 | /// 7 | /// Represents a dependency on a value provided by a provider node higher in 8 | /// the current scene tree. This attribute should be applied to a property of 9 | /// a dependent node. 10 | /// 11 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] 12 | public class DependencyAttribute : Attribute { } 13 | #pragma warning restore 14 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/auto_inject/dependent/DependencyExceptions.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable 2 | namespace Chickensoft.AutoInject; 3 | 4 | using System; 5 | 6 | /// 7 | /// Exception thrown when a provider node cannot be found 8 | /// in any of the dependent node's ancestors while resolving dependencies. 9 | /// 10 | public class ProviderNotFoundException : InvalidOperationException { 11 | /// Creates a new provider not found exception. 12 | /// Provider type. 13 | public ProviderNotFoundException(Type providerType) : base( 14 | $"No provider found for the following type: {providerType}" + ". " + 15 | "Consider specifying a fallback value in `DependOn(T fallback)`." 16 | ) { } 17 | } 18 | 19 | /// 20 | /// Exception thrown when a dependency is accessed before the provider has 21 | /// called . 22 | /// 23 | public class ProviderNotInitializedException : InvalidOperationException { 24 | /// Creates a new provider has not provided exception. 25 | /// Provider type. 26 | public ProviderNotInitializedException(Type providerType) : base( 27 | "The provider for the following type has not called Provide() yet: " + 28 | $"{providerType}" + ". Please call Provide() from the provider " + 29 | "once all of its dependencies have been initialized." 30 | ) { } 31 | } 32 | #pragma warning restore 33 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/auto_inject/dependent/DependencyResolver.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Chickensoft.AutoInject; 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using Godot; 7 | 8 | #pragma warning disable IDE0005 9 | using Chickensoft.AutoInject; 10 | using Chickensoft.Introspection; 11 | using System.Globalization; 12 | 13 | /// 14 | /// Actual implementation of the dependency resolver. 15 | /// 16 | public static class DependencyResolver { 17 | /// 18 | /// A type receiver for use with introspective node's reflection metadata. 19 | /// This is given a class at construction time and used to determine if the 20 | /// class can provide a value of a given type. 21 | /// 22 | private sealed class ProviderValidator : ITypeReceiver { 23 | /// Provider to validate. 24 | public IBaseProvider Provider { get; set; } 25 | 26 | /// 27 | /// Result of the validation. True if the node can provide the type. 28 | /// 29 | public bool Result { get; set; } 30 | 31 | public ProviderValidator() { 32 | Provider = default!; 33 | } 34 | 35 | #nullable disable 36 | public void Receive() => Result = Provider is IProvide; 37 | #nullable restore 38 | } 39 | 40 | /// 41 | /// Essentially a typedef for a Dictionary that maps types to providers. 42 | /// 43 | public class DependencyTable : Dictionary { } 44 | 45 | [ThreadStatic] 46 | private static readonly ProviderValidator _validator; 47 | 48 | static DependencyResolver() { 49 | _validator = new(); 50 | } 51 | 52 | /// 53 | /// The provider validator. This receives the generic type of the provider 54 | /// and uses it to determine if the provider can provide the type of value 55 | /// requested by the dependent. Because we only have one validator and its 56 | /// state is mutated to avoid extra allocations, there is one validator per 57 | /// thread to guarantee safety. 58 | /// 59 | private static ProviderValidator Validator => _validator; 60 | 61 | /// 62 | /// Finds and returns the members of a script that are marked with the 63 | /// [Dependency] attribute. 64 | /// 65 | /// Script members. 66 | /// Members that represent dependencies. 67 | private static IEnumerable GetDependenciesToResolve( 68 | IEnumerable properties 69 | ) { 70 | foreach (var property in properties) { 71 | if (property.Attributes.ContainsKey(typeof(DependencyAttribute))) { 72 | yield return property; 73 | } 74 | } 75 | } 76 | 77 | /// 78 | /// Called by the Dependent mixin on an introspective node to determine if 79 | /// dependencies are stale and need to be resolved. If so, this will 80 | /// automatically trigger the dependency resolution process. 81 | /// 82 | /// Godot node notification. 83 | /// Dependent node. 84 | /// All dependencies. 85 | public static void OnDependent( 86 | int what, 87 | IDependent dependent, 88 | IEnumerable properties 89 | ) { 90 | var state = dependent.MixinState.Get(); 91 | if (what == Node.NotificationExitTree) { 92 | dependent.MixinState.Get().ShouldResolveDependencies = true; 93 | foreach (var pending in state.Pending.Values) { 94 | pending.Unsubscribe(); 95 | } 96 | state.Pending.Clear(); 97 | } 98 | if ( 99 | what == Node.NotificationReady && 100 | state.ShouldResolveDependencies 101 | ) { 102 | Resolve(dependent, properties); 103 | } 104 | } 105 | 106 | /// 107 | /// Returns a dependency that was resolved from an ancestor provider node, 108 | /// or the provided fallback value returned from the given lambda. 109 | /// 110 | /// The type of the value to resolve. 111 | /// Dependent node. 112 | /// Function which returns a fallback value to use if 113 | /// a provider for this type wasn't found during dependency resolution. 114 | /// 115 | /// 116 | /// The resolved dependency value, the fallback value, or throws an exception 117 | /// if the provider wasn't found during dependency resolution and a fallback 118 | /// value was not given 119 | /// 120 | /// Thrown if the provider for 121 | /// the requested value could not be found and when no fallback value is 122 | /// specified. 123 | /// Thrown if a dependency 124 | /// is accessed before the provider has called Provide(). 125 | public static TValue DependOn( 126 | IDependent dependent, Func? fallback = default 127 | ) where TValue : notnull { 128 | // First, check dependency fakes. Using a faked value takes priority over 129 | // all the other dependency resolution methods. 130 | var state = dependent.MixinState.Get(); 131 | if (state.ProviderFakes.TryGetValue(typeof(TValue), out var fakeProvider) 132 | && fakeProvider is DefaultProvider faker) { 133 | return faker.Value(); 134 | } 135 | 136 | // Lookup dependency, per usual, respecting any fallback values if there 137 | // were no resolved providers for the requested type during dependency 138 | // resolution. 139 | if (state.Dependencies.TryGetValue( 140 | typeof(TValue), out var providerNode 141 | ) 142 | ) { 143 | if (!providerNode.ProviderState.IsInitialized) { 144 | throw new ProviderNotInitializedException(typeof(TValue)); 145 | } 146 | if (providerNode is IProvide provider) { 147 | return provider.Value(); 148 | } 149 | else if (providerNode is DefaultProvider defaultProvider) { 150 | return defaultProvider.Value(); 151 | } 152 | } 153 | else if (fallback is not null) { 154 | // See if we were given a fallback. 155 | var value = fallback(); 156 | var provider = new DefaultProvider(value, fallback); 157 | state.Dependencies.Add(typeof(TValue), provider); 158 | return provider.Value(); 159 | } 160 | 161 | throw new ProviderNotFoundException(typeof(TValue)); 162 | } 163 | 164 | /// 165 | /// Resolve dependencies. Used by the Dependent mixin to resolve 166 | /// dependencies for a given introspective node. 167 | /// 168 | /// Introspective node which wants to resolve 169 | /// dependencies. 170 | /// Properties of the introspective node. 171 | /// 172 | private static void Resolve( 173 | IDependent dependent, 174 | IEnumerable properties 175 | ) { 176 | var state = dependent.MixinState.Get(); 177 | // Clear any previously resolved dependencies — if the ancestor tree hasn't 178 | // changed above us, we will just end up re-resolving them as they were. 179 | state.Dependencies.Clear(); 180 | 181 | var shouldResolve = true; 182 | var remainingDependencies = new HashSet( 183 | GetDependenciesToResolve(properties) 184 | ); 185 | 186 | var self = (Node)dependent; 187 | var node = self.GetParent(); 188 | var foundDependencies = new HashSet(); 189 | var providersInitializing = 0; 190 | 191 | void resolve() { 192 | if (self.IsNodeReady()) { 193 | // Godot node is already ready. 194 | if (!dependent.IsTesting) { 195 | dependent.Setup(); 196 | } 197 | dependent.OnResolved(); 198 | return; 199 | } 200 | 201 | // Godot node is not ready yet, so we will wait for OnReady before 202 | // calling Setup() and OnResolved(). 203 | 204 | if (!dependent.IsTesting) { 205 | state.PleaseCallSetup = true; 206 | } 207 | state.PleaseCallOnResolved = true; 208 | } 209 | 210 | void onProviderInitialized(IBaseProvider provider) { 211 | providersInitializing--; 212 | 213 | lock (state.Pending) { 214 | state.Pending[provider].Unsubscribe(); 215 | state.Pending.Remove(provider); 216 | } 217 | 218 | if (providersInitializing == 0) { 219 | resolve(); 220 | } 221 | } 222 | 223 | while (node != null && shouldResolve) { 224 | foundDependencies.Clear(); 225 | 226 | if (node is IBaseProvider provider) { 227 | // For each provider node ancestor, check each of our remaining 228 | // dependencies to see if the provider node is the type needed 229 | // to satisfy the dependency. 230 | foreach (var property in remainingDependencies) { 231 | Validator.Provider = provider; 232 | 233 | // Use the generated introspection metadata to determine if 234 | // we have found the correct provider for the dependency. 235 | property.TypeNode.GenericTypeGetter(Validator); 236 | var isCorrectProvider = Validator.Result; 237 | 238 | if (isCorrectProvider) { 239 | // Add the provider to our internal dependency table. 240 | state.Dependencies.Add( 241 | property.TypeNode.ClosedType, provider 242 | ); 243 | 244 | // Mark this dependency to be removed from the list of dependencies 245 | // we're searching for. 246 | foundDependencies.Add(property); 247 | 248 | // If the provider is not yet initialized, subscribe to its 249 | // initialization event and add it to the list of pending 250 | // subscriptions. 251 | if ( 252 | !provider.ProviderState.IsInitialized && 253 | !state.Pending.ContainsKey(provider) 254 | ) { 255 | state.Pending[provider] = 256 | new PendingProvider(provider, onProviderInitialized); 257 | provider.ProviderState.OnInitialized += onProviderInitialized; 258 | providersInitializing++; 259 | } 260 | } 261 | } 262 | } 263 | 264 | // Remove the dependencies we've resolved. 265 | remainingDependencies.ExceptWith(foundDependencies); 266 | 267 | if (remainingDependencies.Count == 0) { 268 | // Found all dependencies, exit loop. 269 | shouldResolve = false; 270 | } 271 | else { 272 | // Still need to find dependencies — continue up the tree until 273 | // this returns null. 274 | node = node.GetParent(); 275 | } 276 | } 277 | 278 | if (state.Pending.Count == 0) { 279 | // Inform dependent that dependencies have been resolved. 280 | resolve(); 281 | } 282 | 283 | // We *could* check to see if a provider for every dependency was found 284 | // and throw an exception if any were missing, but this would break support 285 | // for fallback values. 286 | } 287 | 288 | public class DefaultProvider : IBaseProvider { 289 | internal object _value; 290 | private readonly Func _fallback; 291 | public ProviderState ProviderState { get; } 292 | 293 | // When working with reference types, we must wrap the value in a 294 | // WeakReference() to allow the garbage collection to work when the 295 | // assembly is being unloaded or reloaded; such as in the case of 296 | // rebuilding within the Godot Editor if you've instantiated a node 297 | // and run it as a tool script. 298 | public DefaultProvider(object value, Func? fallback = default) { 299 | _fallback = fallback ?? (() => (TValue?)value); 300 | 301 | _value = value.GetType().IsValueType 302 | ? value 303 | : new WeakReference(value); 304 | 305 | ProviderState = new() { IsInitialized = true }; 306 | } 307 | 308 | public TValue Value() { 309 | if (_value is WeakReference weakReference) { 310 | // Try to return a reference type. 311 | if (weakReference.Target is TValue target) { 312 | return target; 313 | } 314 | 315 | var value = _fallback() ?? 316 | throw new InvalidOperationException( 317 | "Fallback cannot create a null value" 318 | ); 319 | 320 | _value = new WeakReference(value); 321 | 322 | return value; 323 | 324 | } 325 | // Return a value type. 326 | return (TValue)_value; 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/auto_inject/dependent/DependentExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject; 2 | 3 | using System; 4 | #pragma warning disable IDE0005 5 | using Chickensoft.AutoInject; 6 | 7 | public static class DependentExtensions { 8 | /// 10 | public static TValue DependOn( 11 | this IDependent dependent, 12 | Func? fallback = default 13 | ) where TValue : notnull => DependencyResolver.DependOn(dependent, fallback); 14 | 15 | /// 16 | public static void FakeDependency( 17 | this IDependent dependent, T value 18 | ) where T : notnull => dependent.FakeDependency(value); 19 | } 20 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/auto_inject/dependent/DependentState.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | #pragma warning disable IDE0005 7 | using Chickensoft.AutoInject; 8 | using Chickensoft.Introspection; 9 | using System.Globalization; 10 | 11 | /// 12 | /// Dependent introspective nodes are all given a private dependency state which 13 | /// stores the dependency table and a flag indicating if dependencies are 14 | /// stale. This is the only pointer that is added to each dependent node to 15 | /// avoid increasing the memory footprint of nodes. 16 | /// 17 | public class DependentState { 18 | /// 19 | /// True if the node is being unit-tested. When unit-tested, setup callbacks 20 | /// will not be invoked. 21 | /// 22 | public bool IsTesting { get; set; } 23 | 24 | /// 25 | /// Resolved dependencies are stored in this table. Don't touch! 26 | /// 27 | public readonly DependencyResolver.DependencyTable Dependencies = []; 28 | 29 | /// 30 | /// Used by the dependency system to determine if dependencies are stale. 31 | /// Dependencies go stale whenever a node is removed from the tree and added 32 | /// back. 33 | /// 34 | public bool ShouldResolveDependencies { get; set; } = true; 35 | 36 | /// Set internally when Setup() should be called. 37 | public bool PleaseCallSetup { get; set; } 38 | /// Set internally when OnResolved() should be called. 39 | public bool PleaseCallOnResolved { get; set; } 40 | 41 | /// 42 | /// Dictionary of providers we are listening to that are still initializing 43 | /// their provided values. We use this in the rare event that we have to 44 | /// clean up subscriptions before providers ever finished initializing. 45 | /// 46 | public Dictionary Pending { get; } 47 | = []; 48 | 49 | /// 50 | /// Overrides for providers keyed by dependency type. Overriding providers 51 | /// allows nodes being unit-tested to provide fake providers during unit tests 52 | /// that return mock or faked values. 53 | /// 54 | public DependencyResolver.DependencyTable ProviderFakes { 55 | get; 56 | } = []; 57 | } 58 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/auto_inject/dependent/IDependent.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject; 2 | 3 | using Godot; 4 | 5 | #pragma warning disable IDE0005 6 | using Chickensoft.AutoInject; 7 | using Chickensoft.Introspection; 8 | using System.Globalization; 9 | using System; 10 | using System.Runtime.CompilerServices; 11 | 12 | 13 | /// 14 | /// Dependent mixin. Apply this to an introspective node to automatically 15 | /// resolve dependencies marked with the [Dependency] attribute. 16 | /// 17 | [Mixin] 18 | public interface IDependent : IMixin, IAutoInit, IReadyAware { 19 | DependentState DependentState { 20 | get { 21 | AddStateIfNeeded(); 22 | return MixinState.Get(); 23 | } 24 | } 25 | 26 | /// 27 | /// Called after dependencies are resolved, but before 28 | /// is called if (and only if) 29 | /// is false. This allows you to initialize 30 | /// properties that depend on dependencies separate from using those 31 | /// properties to facilitate easier testing. 32 | /// 33 | void Setup() { } 34 | 35 | /// 36 | /// Method that is invoked when all of the dependent node's dependencies are 37 | /// resolved (after _Ready() but before _Process()). 38 | /// 39 | void OnResolved() { } 40 | 41 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 42 | void IReadyAware.OnBeforeReady() { } 43 | 44 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 45 | void IReadyAware.OnAfterReady() { 46 | if (DependentState.PleaseCallSetup) { 47 | Setup(); 48 | DependentState.PleaseCallSetup = false; 49 | } 50 | if (DependentState.PleaseCallOnResolved) { 51 | OnResolved(); 52 | DependentState.PleaseCallOnResolved = false; 53 | } 54 | } 55 | 56 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 57 | private void AddStateIfNeeded() { 58 | if (MixinState.Has()) { return; } 59 | 60 | MixinState.Overwrite(new DependentState()); 61 | } 62 | 63 | void IMixin.Handler() { } 64 | 65 | // Specifying "new void" makes this hide the existing handler, which works 66 | // since the introspection generator calls us as ((IDependent)obj).Handler() 67 | // rather than ((IMixin)obj).Handler(). 68 | public new void Handler() { 69 | if (this is not Node node) { 70 | return; 71 | } 72 | 73 | node.__SetupNotificationStateIfNeeded(); 74 | AddStateIfNeeded(); 75 | 76 | if ( 77 | this is IIntrospectiveRef 78 | introspective && 79 | !introspective.HasMixin(typeof(IAutoInit)) 80 | ) { 81 | // Developer didn't give us the IAutoInit mixin, but all dependents are 82 | // required to also be IAutoInit. So we'll invoke it for them manually. 83 | (this as IAutoInit).Handler(); 84 | } 85 | 86 | DependencyResolver.OnDependent( 87 | MixinState.Get().Notification, 88 | this, 89 | Types.Graph.GetProperties(GetType()) 90 | ); 91 | } 92 | 93 | /// 94 | /// Add a fake value to the dependency table. Adding a fake value allows a 95 | /// unit test to override a dependency lookup with a fake value. 96 | /// 97 | /// Dependency value (probably a mock or a fake). 98 | /// Dependency type. 99 | public void FakeDependency(T value) where T : notnull { 100 | AddStateIfNeeded(); 101 | MixinState.Get().ProviderFakes[typeof(T)] = 102 | new DependencyResolver.DefaultProvider(value); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/auto_inject/dependent/PendingProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject; 2 | 3 | using System; 4 | 5 | #pragma warning disable IDE0005 6 | using Chickensoft.AutoInject; 7 | using Chickensoft.Introspection; 8 | using System.Globalization; 9 | 10 | public class PendingProvider( 11 | IBaseProvider provider, Action success 12 | ) { 13 | public IBaseProvider Provider { get; } = provider; 14 | public Action Success { get; } = success; 15 | public void Unsubscribe() => Provider.ProviderState.OnInitialized -= Success; 16 | } 17 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/auto_inject/provider/IProvide.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable 2 | namespace Chickensoft.AutoInject; 3 | 4 | using System; 5 | using Godot; 6 | using Chickensoft.Introspection; 7 | using Chickensoft.AutoInject; 8 | 9 | /// Base provider interface used internally by AutoInject. 10 | public interface IBaseProvider { 11 | /// Provider state. 12 | ProviderState ProviderState { get; } 13 | } 14 | 15 | /// 16 | /// A provider of a value of type . 17 | /// 18 | /// The type of value provided. To prevent pain, providers 19 | /// should not provide a value that could ever be null. 20 | public interface IProvide : IProvider where T : notnull { 21 | /// Value that is provided by the provider. 22 | T Value(); 23 | } 24 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/auto_inject/provider/IProvider.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable 2 | namespace Chickensoft.AutoInject; 3 | 4 | using System; 5 | using Godot; 6 | using Chickensoft.Introspection; 7 | using Chickensoft.AutoInject; 8 | 9 | /// 10 | /// Turns an ordinary node into a provider node. 11 | /// 12 | [Mixin] 13 | public interface IProvider : IMixin, IBaseProvider { 14 | /// 15 | ProviderState IBaseProvider.ProviderState { 16 | get { 17 | AddStateIfNeeded(); 18 | return MixinState.Get(); 19 | } 20 | } 21 | 22 | /// 23 | /// When a provider has initialized all of the values it provides, this method 24 | /// is invoked on the provider itself (immediately after _Ready). When this 25 | /// method is called, the provider is guaranteed that all of its descendant 26 | /// nodes that depend this provider have resolved their dependencies. 27 | /// 28 | void OnProvided() { } 29 | 30 | /// 31 | /// 32 | /// Call this method once all your dependencies have been initialized. This 33 | /// will inform any dependent nodes that are waiting on this provider that 34 | /// the provider has finished initializing. 35 | /// 36 | /// 37 | /// Forgetting to call this method can prevent dependencies from resolving 38 | /// correctly throughout the scene tree. 39 | /// 40 | /// 41 | public void Provide() => ProviderState.Provide(this); 42 | 43 | void IMixin.Handler() { 44 | ProviderState.OnProvider( 45 | MixinState.Get().Notification, this 46 | ); 47 | } 48 | 49 | private void AddStateIfNeeded() { 50 | if (MixinState.Has()) { return; } 51 | MixinState.Set(new ProviderState()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/auto_inject/provider/ProviderExtensions.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable 2 | namespace Chickensoft.AutoInject; 3 | 4 | using System; 5 | using Godot; 6 | using Chickensoft.Introspection; 7 | using Chickensoft.AutoInject; 8 | 9 | public static class ProviderExtensions { 10 | public static void Provide(this IProvider provider) { 11 | provider.Provide(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/auto_inject/provider/ProviderState.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject; 2 | 3 | using System; 4 | using Godot; 5 | #pragma warning disable IDE0005 6 | using Chickensoft.Introspection; 7 | using Chickensoft.AutoInject; 8 | 9 | 10 | /// 11 | /// Provider state used internally when resolving dependencies. 12 | /// 13 | public class ProviderState { 14 | /// Whether the provider has initialized all of its values. 15 | public bool IsInitialized { get; set; } 16 | 17 | /// 18 | /// Underlying event delegate used to inform dependent nodes that the 19 | /// provider has initialized all of the values it provides. 20 | /// 21 | public event Action? OnInitialized; 22 | 23 | /// 24 | /// Announces to descendent nodes that the values provided by this provider 25 | /// are initialized. 26 | /// 27 | /// Provider node which has finished initializing 28 | /// the values it provides. 29 | public void Announce(IBaseProvider provider) 30 | => OnInitialized?.Invoke(provider); 31 | 32 | /// 33 | /// Internal implementation for the OnProvider lifecycle method. Resets the 34 | /// provider's initialized status when the provider leaves the scene tree. 35 | /// 36 | /// Godot node notification. 37 | /// Provider node. 38 | public static void OnProvider(int what, IProvider provider) { 39 | if (what == Node.NotificationExitTree) { 40 | provider.ProviderState.IsInitialized = false; 41 | } 42 | } 43 | 44 | /// 45 | /// Internal implementation for the Provide method. This marks the Provider 46 | /// as having provided all of its values and then announces to dependent 47 | /// nodes that the provider has finished initializing. 48 | /// 49 | /// 50 | public static void Provide(IProvider provider) { 51 | provider.ProviderState.IsInitialized = true; 52 | provider.ProviderState.Announce(provider); 53 | provider.OnProvided(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/auto_node/AutoNode.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject; 2 | 3 | using Chickensoft.Introspection; 4 | 5 | /// 6 | /// 7 | /// Add this mixin to your introspective node to automatically connects nodes 8 | /// declared with the [Node] attribute, 9 | /// call an additional initialization lifecycle method, and allow you to 10 | /// provide dependencies to descendant nodes or fetch them from ancestors via 11 | /// the [Dependency] attribute. 12 | /// 13 | /// 14 | /// This enables you to leverage all of the functionality of AutoInject with one 15 | /// easy mixin. 16 | /// 17 | /// 18 | public interface IAutoNode : IMixin, 19 | IAutoOn, IAutoConnect, IAutoInit, IProvider, IDependent { 20 | void IMixin.Handler() { } 21 | 22 | new void Handler() { 23 | // IAutoOn isn't called since its handler does nothing. 24 | (this as IAutoConnect).Handler(); 25 | // IDependent invokes IAutoInit, so we don't invoke it directly. 26 | (this as IProvider).Handler(); 27 | (this as IDependent).Handler(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/misc/IReadyAware.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject; 2 | 3 | /// 4 | /// Types that want to be informed of ready can implement this interface. 5 | /// 6 | public interface IReadyAware { 7 | /// Called right before the node is ready. 8 | void OnBeforeReady(); 9 | 10 | /// Called right after the node is readied. 11 | void OnAfterReady(); 12 | } 13 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/notifications/NotificationExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject; 2 | 3 | using System.Runtime.CompilerServices; 4 | using Chickensoft.Introspection; 5 | using Godot; 6 | 7 | public static class NotificationExtensions { 8 | /// 9 | /// Notify mixins applied to a Godot object that a notification has been 10 | /// received. 11 | /// 12 | /// Godot object. 13 | /// Godot object notification. 14 | public static void Notify(this GodotObject obj, int what) { 15 | obj.__SetupNotificationStateIfNeeded(); 16 | 17 | if (obj is not IIntrospectiveRef introspective) { 18 | return; 19 | } 20 | 21 | // Share the notification that just occurred with the mixins we're 22 | // about to invoke. 23 | introspective.MixinState.Get().Notification = what; 24 | 25 | // Invoke each mixin's handler method. 26 | introspective.InvokeMixins(); 27 | 28 | // If we're an IAutoOn, invoke the notification methods like OnReady, 29 | // OnProcess, etc. We specifically do this last. 30 | if (obj is IAutoOn autoOn) { 31 | IAutoOn.InvokeNotificationMethods(introspective, what); 32 | } 33 | } 34 | 35 | #pragma warning disable IDE1006 36 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 37 | public static void __SetupNotificationStateIfNeeded(this GodotObject obj) { 38 | if (obj is not IIntrospectiveRef introspective) { 39 | return; 40 | } 41 | 42 | if (!introspective.MixinState.Has()) { 43 | introspective.MixinState.Overwrite(new NotificationState()); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/src/notifications/NotificationState.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject; 2 | 3 | public class NotificationState { 4 | /// 5 | /// Most recently received Godot object notification. 6 | /// 7 | public int Notification { get; set; } = -1; 8 | } 9 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/Tests.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests; 2 | 3 | using System.Reflection; 4 | using Chickensoft.GoDotTest; 5 | using Godot; 6 | 7 | public partial class Tests : Node2D { 8 | public override void _Ready() => CallDeferred(MethodName.RunTests); 9 | 10 | public void RunTests() => 11 | GoTest.RunTests(Assembly.GetExecutingAssembly(), this); 12 | } 13 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/Tests.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://bv5dxd8hrc5g4"] 2 | 3 | [ext_resource type="Script" path="res://test/Tests.cs" id="1_310o6"] 4 | 5 | [node name="Node2D" type="Node2D"] 6 | script = ExtResource("1_310o6") 7 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/fixtures/AutoConnectInvalidCastTestScene.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests.Fixtures; 2 | 3 | using Chickensoft.GodotNodeInterfaces; 4 | using Chickensoft.Introspection; 5 | using Chickensoft.AutoInject; 6 | using Godot; 7 | 8 | [Meta(typeof(IAutoConnect))] 9 | public partial class AutoConnectInvalidCastTestScene : Node2D { 10 | public override void _Notification(int what) => this.Notify(what); 11 | 12 | [Node("Node3D")] 13 | public INode2D Node { get; set; } = default!; 14 | } 15 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/fixtures/AutoConnectInvalidCastTestScene.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://buifevchck4xm"] 2 | 3 | [ext_resource type="Script" path="res://test/fixtures/AutoConnectInvalidCastTestScene.cs" id="1_1ef0r"] 4 | 5 | [node name="AutoConnectInvalidCastTestScene" type="Node2D"] 6 | script = ExtResource("1_1ef0r") 7 | 8 | [node name="Node3D" type="Node3D" parent="."] 9 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/fixtures/AutoConnectMissingTestScene.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests.Fixtures; 2 | 3 | using Chickensoft.GodotNodeInterfaces; 4 | using Chickensoft.Introspection; 5 | using Chickensoft.AutoInject; 6 | using Godot; 7 | 8 | [Meta(typeof(IAutoConnect))] 9 | public partial class AutoConnectMissingTestScene : Node2D { 10 | public override void _Notification(int what) => this.Notify(what); 11 | 12 | [Node("NonExistentNode")] 13 | public INode2D MyNode { get; set; } = default!; 14 | } 15 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/fixtures/AutoConnectMissingTestScene.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://gvbk8s4ox36p"] 2 | 3 | [ext_resource type="Script" path="res://test/fixtures/AutoConnectMissingTestScene.cs" id="1_5ywa5"] 4 | 5 | [node name="AutoConnectMissingTestScene" type="Node2D"] 6 | script = ExtResource("1_5ywa5") 7 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/fixtures/AutoConnectTestScene.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests.Fixtures; 2 | 3 | using Chickensoft.GodotNodeInterfaces; 4 | using Chickensoft.Introspection; 5 | using Chickensoft.AutoInject; 6 | using Godot; 7 | 8 | [Meta(typeof(IAutoConnect))] 9 | public partial class AutoConnectTestScene : Node2D { 10 | public override void _Notification(int what) => this.Notify(what); 11 | 12 | [Node("Path/To/MyNode")] 13 | public INode2D MyNode { get; set; } = default!; 14 | 15 | [Node("Path/To/MyNode")] 16 | public Node2D MyNodeOriginal { get; set; } = default!; 17 | 18 | [Node] 19 | public INode2D MyUniqueNode { get; set; } = default!; 20 | 21 | [Node("%OtherUniqueName")] 22 | public INode2D DifferentName { get; set; } = default!; 23 | 24 | #pragma warning disable IDE1006 25 | [Node] 26 | internal INode2D _my_unique_node { get; set; } = default!; 27 | 28 | [Other] 29 | public INode2D SomeOtherNodeReference { get; set; } = default!; 30 | } 31 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/fixtures/AutoConnectTestScene.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://bc0rrd0etom5k"] 2 | 3 | [ext_resource type="Script" path="res://test/fixtures/AutoConnectTestScene.cs" id="1_ego6e"] 4 | 5 | [node name="AutoConnectTestScene" type="Node2D"] 6 | script = ExtResource("1_ego6e") 7 | 8 | [node name="Path" type="Node2D" parent="."] 9 | 10 | [node name="To" type="Node2D" parent="Path"] 11 | 12 | [node name="MyNode" type="Node2D" parent="Path/To"] 13 | 14 | [node name="MyUniqueNode" type="Node2D" parent="."] 15 | unique_name_in_owner = true 16 | 17 | [node name="OtherUniqueName" type="Node2D" parent="."] 18 | unique_name_in_owner = true 19 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/fixtures/AutoSetupTestNode.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests.Fixtures; 2 | 3 | using Chickensoft.Introspection; 4 | using Chickensoft.AutoInject; 5 | using Godot; 6 | 7 | [Meta(typeof(IAutoInit))] 8 | public partial class AutoInitTestNode : Node2D { 9 | public override void _Notification(int what) => this.Notify(what); 10 | 11 | public int Called { get; set; } 12 | 13 | public void Initialize() => Called++; 14 | } 15 | 16 | [Meta(typeof(IAutoNode))] 17 | public partial class AutoInitTestAutoNode : Node2D { 18 | public override void _Notification(int what) => this.Notify(what); 19 | 20 | public int Called { get; set; } 21 | 22 | public void Initialize() => Called++; 23 | } 24 | 25 | [Meta(typeof(IAutoInit))] 26 | public partial class AutoInitTestNodeNoImplementation : Node2D { 27 | public override void _Notification(int what) => this.Notify(what); 28 | } 29 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/fixtures/Dependents.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests.Subjects; 2 | 3 | using System; 4 | using Chickensoft.Introspection; 5 | using Godot; 6 | 7 | [Meta(typeof(IAutoOn), typeof(IDependent))] 8 | public partial class StringDependent : Node { 9 | public override void _Notification(int what) => this.Notify(what); 10 | 11 | [Dependency] 12 | public string MyDependency => this.DependOn(); 13 | 14 | public bool OnResolvedCalled { get; private set; } 15 | public string ResolvedValue { get; set; } = ""; 16 | 17 | public void OnReady() { } 18 | 19 | public void OnResolved() { 20 | OnResolvedCalled = true; 21 | ResolvedValue = MyDependency; 22 | } 23 | } 24 | 25 | [Meta(typeof(IAutoOn), typeof(IDependent))] 26 | public partial class FakedDependent : Node { 27 | public override void _Notification(int what) => this.Notify(what); 28 | 29 | [Dependency] 30 | public string MyDependency => this.DependOn(() => "fallback"); 31 | 32 | public bool OnResolvedCalled { get; private set; } 33 | public string ResolvedValue { get; set; } = ""; 34 | 35 | public void OnResolved() { 36 | OnResolvedCalled = true; 37 | ResolvedValue = MyDependency; 38 | } 39 | } 40 | 41 | [Meta(typeof(IAutoOn), typeof(IDependent))] 42 | public partial class StringDependentFallback : Node { 43 | public override void _Notification(int what) => this.Notify(what); 44 | 45 | [Dependency] 46 | public string MyDependency => this.DependOn(() => FallbackValue); 47 | 48 | public string FallbackValue { get; set; } = ""; 49 | public bool OnResolvedCalled { get; private set; } 50 | public string ResolvedValue { get; set; } = ""; 51 | 52 | public void OnReady() { } 53 | 54 | public void OnResolved() { 55 | OnResolvedCalled = true; 56 | ResolvedValue = MyDependency; 57 | } 58 | } 59 | 60 | [Meta(typeof(IAutoOn), typeof(IDependent))] 61 | public partial class WeakReferenceDependent : Node { 62 | public override void _Notification(int what) => this.Notify(what); 63 | 64 | [Dependency] 65 | public object MyDependency => this.DependOn(Fallback); 66 | 67 | public Func? Fallback { get; set; } 68 | public bool OnResolvedCalled { get; private set; } 69 | 70 | public void OnReady() { } 71 | 72 | public void OnResolved() => OnResolvedCalled = true; 73 | } 74 | 75 | [Meta(typeof(IAutoOn), typeof(IDependent))] 76 | public partial class ReferenceDependentFallback : Node { 77 | public override void _Notification(int what) => this.Notify(what); 78 | 79 | [Dependency] 80 | public object MyDependency => this.DependOn(() => FallbackValue); 81 | 82 | public object FallbackValue { get; set; } = new Resource(); 83 | public bool OnResolvedCalled { get; private set; } 84 | public object ResolvedValue { get; set; } = null!; 85 | 86 | public void OnReady() { } 87 | 88 | public void OnResolved() { 89 | OnResolvedCalled = true; 90 | ResolvedValue = MyDependency; 91 | } 92 | } 93 | 94 | [Meta(typeof(IAutoOn), typeof(IDependent))] 95 | public partial class IntDependent : Node { 96 | public override void _Notification(int what) => this.Notify(what); 97 | 98 | [Dependency] 99 | public int MyDependency => this.DependOn(FallbackValue); 100 | 101 | public bool OnResolvedCalled { get; private set; } 102 | public int ResolvedValue { get; set; } 103 | public Func? FallbackValue { get; set; } 104 | 105 | public void OnReady() { } 106 | 107 | public void OnResolved() { 108 | OnResolvedCalled = true; 109 | ResolvedValue = MyDependency; 110 | } 111 | } 112 | 113 | [Meta(typeof(IAutoOn), typeof(IDependent))] 114 | public partial class MultiDependent : Node { 115 | public override void _Notification(int what) => this.Notify(what); 116 | 117 | [Dependency] 118 | public int IntDependency => this.DependOn(); 119 | 120 | [Dependency] 121 | public string StringDependency => this.DependOn(); 122 | 123 | public bool OnResolvedCalled { get; private set; } 124 | public int IntResolvedValue { get; set; } 125 | public string StringResolvedValue { get; set; } = null!; 126 | public bool ReadyCalled { get; set; } 127 | public void OnReady() => ReadyCalled = true; 128 | 129 | public void OnResolved() { 130 | OnResolvedCalled = true; 131 | IntResolvedValue = IntDependency; 132 | StringResolvedValue = StringDependency; 133 | } 134 | } 135 | 136 | [Meta(typeof(IAutoOn), typeof(IDependent))] 137 | public partial class NoDependenciesDependent : Node { 138 | public override void _Notification(int what) => this.Notify(what); 139 | 140 | public bool OnResolvedCalled { get; private set; } 141 | 142 | public void OnResolved() => OnResolvedCalled = true; 143 | } 144 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/fixtures/MultiProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests.Fixtures; 2 | 3 | using Chickensoft.AutoInject; 4 | using Chickensoft.AutoInject.Tests.Subjects; 5 | using Chickensoft.Introspection; 6 | using Godot; 7 | 8 | [Meta(typeof(IAutoOn), typeof(IProvider))] 9 | public partial class MultiProvider : Node2D, IProvide, IProvide { 10 | public override void _Notification(int what) => this.Notify(what); 11 | 12 | int IProvide.Value() => IntValue; 13 | string IProvide.Value() => StringValue; 14 | 15 | public MultiDependent Child { get; private set; } = default!; 16 | 17 | public override void _Ready() { 18 | Child = new MultiDependent(); 19 | AddChild(Child); 20 | 21 | this.Provide(); 22 | } 23 | 24 | public bool OnProvidedCalled { get; private set; } 25 | public int IntValue { get; set; } 26 | public string StringValue { get; set; } = ""; 27 | 28 | public void OnProvided() => OnProvidedCalled = true; 29 | } 30 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/fixtures/MultiProvider.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://bby7an0ngn7l6"] 2 | 3 | [ext_resource type="Script" path="res://test/fixtures/MultiProvider.cs" id="1_gn2k1"] 4 | 5 | [node name="MultiProvider" type="Node2D"] 6 | script = ExtResource("1_gn2k1") 7 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/fixtures/MyNode.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests.Fixtures; 2 | 3 | using Chickensoft.GodotNodeInterfaces; 4 | using Chickensoft.Introspection; 5 | using Chickensoft.AutoInject; 6 | using Godot; 7 | 8 | [Meta(typeof(IAutoConnect))] 9 | public partial class MyNode : Node2D { 10 | public override void _Notification(int what) => this.Notify(what); 11 | 12 | [Node("Path/To/SomeNode")] 13 | public INode2D SomeNode { get; set; } = default!; 14 | 15 | [Node] // Connects to "%MyUniqueNode" since no path was specified. 16 | public INode2D MyUniqueNode { get; set; } = default!; 17 | 18 | [Node("%OtherUniqueName")] 19 | public INode2D DifferentName { get; set; } = default!; 20 | 21 | #pragma warning disable IDE1006 22 | [Node] // Connects to "%MyUniqueNode" since no path was specified. 23 | internal INode2D _my_unique_node { get; set; } = default!; 24 | } 25 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/fixtures/OtherAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests.Fixtures; 2 | 3 | using System; 4 | 5 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] 6 | public class OtherAttribute : Attribute { } 7 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/fixtures/Providers.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests.Subjects; 2 | 3 | using Chickensoft.AutoInject; 4 | using Chickensoft.Introspection; 5 | using Godot; 6 | 7 | // Provider nodes created to be used as test subjects. 8 | 9 | [Meta(typeof(IAutoOn), typeof(IProvider))] 10 | public partial class StringProvider : Node, IProvide { 11 | public override void _Notification(int what) => this.Notify(what); 12 | 13 | string IProvide.Value() => Value; 14 | 15 | public bool OnProvidedCalled { get; private set; } 16 | public string Value { get; set; } = ""; 17 | 18 | public void OnReady() => this.Provide(); 19 | 20 | public void OnProvided() => OnProvidedCalled = true; 21 | } 22 | 23 | [Meta(typeof(IAutoOn), typeof(IProvider))] 24 | public partial class IntProvider : Node, IProvide { 25 | public override void _Notification(int what) => this.Notify(what); 26 | 27 | int IProvide.Value() => Value; 28 | 29 | public void OnReady() => this.Provide(); 30 | 31 | public bool OnProvidedCalled { get; private set; } 32 | public int Value { get; set; } 33 | 34 | public void OnProvided() => OnProvidedCalled = true; 35 | } 36 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/src/AutoConnectInvalidCastTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests; 2 | 3 | using System; 4 | using Chickensoft.GodotNodeInterfaces; 5 | using Chickensoft.GoDotTest; 6 | using Chickensoft.AutoInject.Tests.Fixtures; 7 | using Godot; 8 | using Moq; 9 | using Shouldly; 10 | 11 | public class AutoConnectInvalidCastTest(Node testScene) : TestClass(testScene) { 12 | [Test] 13 | public void ThrowsOnIncorrectNodeType() { 14 | var scene = GD.Load( 15 | "res://test/fixtures/AutoConnectInvalidCastTestScene.tscn" 16 | ); 17 | // AutoNode will actually throw an InvalidCastException 18 | // during the scene instantiation, but for whatever reason that doesn't 19 | // happen on our call stack. So we just make sure the node is null after :/ 20 | var node = scene.Instantiate(); 21 | node.Node.ShouldBeNull(); 22 | } 23 | 24 | [Test] 25 | public void ThrowsIfFakedChildNodeIsWrongType() { 26 | var scene = new AutoConnectInvalidCastTestScene(); 27 | scene.FakeNodeTree(new() { ["Node3D"] = new Mock().Object }); 28 | 29 | Should.Throw( 30 | () => scene._Notification((int)Node.NotificationEnterTree) 31 | ); 32 | } 33 | 34 | [Test] 35 | public void ThrowsIfNoNode() { 36 | var scene = new AutoConnectInvalidCastTestScene(); 37 | Should.Throw( 38 | () => scene._Notification((int)Node.NotificationEnterTree) 39 | ); 40 | } 41 | 42 | [Test] 43 | public void ThrowsIfTypeIsWrong() { 44 | var scene = new AutoConnectInvalidCastTestScene(); 45 | 46 | var node = new Control { 47 | Name = "Node3D" 48 | }; 49 | scene.AddChild(node); 50 | 51 | Should.Throw( 52 | () => scene._Notification((int)Node.NotificationEnterTree) 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/src/AutoConnectMissingTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests; 2 | 3 | using Chickensoft.GoDotTest; 4 | using Chickensoft.AutoInject.Tests.Fixtures; 5 | using Godot; 6 | using Shouldly; 7 | 8 | public class AutoConnectMissingTest(Node testScene) : TestClass(testScene) { 9 | [Test] 10 | public void ThrowsOnMissingNode() { 11 | var scene = GD.Load("res://test/fixtures/AutoConnectMissingTestScene.tscn"); 12 | // AutoNode will actually throw an InvalidOperationException 13 | // during the scene instantiation, but for whatever reason that doesn't 14 | // happen on our call stack. So we just make sure the node is null after :/ 15 | var node = scene.InstantiateOrNull(); 16 | node.MyNode.ShouldBeNull(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/src/AutoConnectTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests; 2 | 3 | using System.Threading.Tasks; 4 | using Chickensoft.GoDotTest; 5 | using Chickensoft.AutoInject.Tests.Fixtures; 6 | using Godot; 7 | using GodotTestDriver; 8 | using Shouldly; 9 | using System; 10 | using Chickensoft.Introspection; 11 | 12 | public partial class AutoConnectTest(Node testScene) : TestClass(testScene) { 13 | private Fixture _fixture = default!; 14 | private AutoConnectTestScene _scene = default!; 15 | 16 | [Meta(typeof(IAutoConnect))] 17 | public partial class NotAGodotNode { } 18 | 19 | [Setup] 20 | public async Task Setup() { 21 | _fixture = new Fixture(TestScene.GetTree()); 22 | _scene = await _fixture.LoadAndAddScene(); 23 | } 24 | 25 | [Cleanup] 26 | public async Task Cleanup() => await _fixture.Cleanup(); 27 | 28 | [Test] 29 | public void ConnectsNodesCorrectlyWhenInstantiated() { 30 | _scene.MyNode.ShouldNotBeNull(); 31 | _scene.MyNodeOriginal.ShouldNotBeNull(); 32 | _scene.MyUniqueNode.ShouldNotBeNull(); 33 | _scene.DifferentName.ShouldNotBeNull(); 34 | _scene._my_unique_node.ShouldNotBeNull(); 35 | _scene.SomeOtherNodeReference.ShouldBeNull(); 36 | } 37 | 38 | [Test] 39 | public void NonAutoConnectNodeThrows() { 40 | var node = new Node(); 41 | Should.Throw(() => node.FakeNodeTree(null)); 42 | } 43 | 44 | [Test] 45 | public void FakeNodesDoesNothingIfGivenNull() { 46 | IAutoConnect node = new AutoConnectTestScene(); 47 | Should.NotThrow(() => node.FakeNodes = null); 48 | } 49 | 50 | [Test] 51 | public void AddStateIfNeededDoesNothingIfNotAGodotNode() { 52 | IAutoConnect node = new NotAGodotNode(); 53 | Should.NotThrow(() => node._AddStateIfNeeded()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/src/AutoInitTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests; 2 | using Chickensoft.GoDotTest; 3 | using Chickensoft.AutoInject.Tests.Fixtures; 4 | using Godot; 5 | using Shouldly; 6 | using Chickensoft.Introspection; 7 | 8 | public partial class AutoInitTest(Node testScene) : TestClass(testScene) { 9 | [Meta(typeof(IAutoInit))] 10 | public partial class NotAGodotNode { } 11 | 12 | [Test] 13 | public void SetsUpNode() { 14 | var node = new AutoInitTestNode(); 15 | 16 | node._Notification((int)Node.NotificationReady); 17 | 18 | node.Called.ShouldBe(1); 19 | } 20 | 21 | [Test] 22 | public void DefaultImplementationDoesNothing() { 23 | var node = new AutoInitTestNodeNoImplementation(); 24 | 25 | node._Notification((int)Node.NotificationReady); 26 | } 27 | 28 | [Test] 29 | public void IsTestingCreatesStateIfSetFirst() { 30 | var node = new AutoInitTestNode(); 31 | (node as IAutoInit).IsTesting = true; 32 | // Should do nothing on a non-ready notification 33 | node._Notification((int)Node.NotificationEnterTree); 34 | } 35 | 36 | [Test] 37 | public void HandlerDoesNotWorkIfNotGodotNode() => Should.NotThrow(() => { 38 | var node = new NotAGodotNode(); 39 | (node as IAutoInit).Handler(); 40 | }); 41 | 42 | [Test] 43 | public void AutoNodeMixinOnlyCallsInitializeOnce() { 44 | var node = new AutoInitTestAutoNode(); 45 | 46 | node._Notification((int)Node.NotificationReady); 47 | 48 | node.Called.ShouldBe(1); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/src/AutoNodeTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests; 2 | using Chickensoft.GoDotTest; 3 | using Chickensoft.Introspection; 4 | using Godot; 5 | using Shouldly; 6 | 7 | public partial class AutoNodeTest(Node testScene) : TestClass(testScene) { 8 | [Meta(typeof(IAutoNode))] 9 | public partial class NotAGodotNode : GodotObject { } 10 | 11 | 12 | [Test] 13 | public void MixinHandlerActuallyDoesNothing() { 14 | IMixin node = new NotAGodotNode(); 15 | 16 | Should.NotThrow(node.Handler); 17 | } 18 | 19 | [Test] 20 | public void CallsOtherMixins() => Should.NotThrow(() => { 21 | 22 | var node = new NotAGodotNode(); 23 | 24 | node.__SetupNotificationStateIfNeeded(); 25 | 26 | IIntrospectiveRef introspective = node; 27 | 28 | // Some mixins need this data. 29 | node.MixinState.Get().Notification = -1; 30 | 31 | introspective.InvokeMixins(); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/src/AutoOnTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests; 2 | 3 | using Chickensoft.GoDotTest; 4 | using Chickensoft.Introspection; 5 | using Godot; 6 | using Shouldly; 7 | 8 | public partial class AutoOnTest(Node testScene) : TestClass(testScene) { 9 | [Meta(typeof(IAutoOn))] 10 | public partial class AutoOnTestNode : Node { } 11 | 12 | [Meta(typeof(IAutoOn))] 13 | public partial class NotAGodotNode { } 14 | 15 | public class NotAutoOn { } 16 | 17 | [Test] 18 | public void DoesNothingIfNotAGodotNode() { 19 | var node = new NotAGodotNode(); 20 | 21 | Should.NotThrow(() => IAutoOn.InvokeNotificationMethods(node, 1)); 22 | } 23 | 24 | [Test] 25 | public void DOesNothingIfNotAutoOn() { 26 | var node = new NotAutoOn(); 27 | 28 | Should.NotThrow(() => IAutoOn.InvokeNotificationMethods(node, 1)); 29 | } 30 | 31 | [Test] 32 | public void InvokesHandlerForNotification() { 33 | var node = new AutoOnTestNode(); 34 | IAutoOn autoNode = node; 35 | 36 | Should.NotThrow(() => { 37 | IAutoOn.InvokeNotificationMethods( 38 | autoNode, (int)GodotObject.NotificationPostinitialize 39 | ); 40 | IAutoOn.InvokeNotificationMethods( 41 | autoNode, (int)GodotObject.NotificationPredelete 42 | ); 43 | IAutoOn.InvokeNotificationMethods( 44 | autoNode, (int)Node.NotificationEnterTree 45 | ); 46 | IAutoOn.InvokeNotificationMethods( 47 | autoNode, (int)Node.NotificationWMWindowFocusIn 48 | ); 49 | IAutoOn.InvokeNotificationMethods( 50 | autoNode, (int)Node.NotificationWMWindowFocusOut 51 | ); 52 | IAutoOn.InvokeNotificationMethods( 53 | autoNode, (int)Node.NotificationWMCloseRequest 54 | ); 55 | IAutoOn.InvokeNotificationMethods( 56 | autoNode, (int)Node.NotificationWMSizeChanged 57 | ); 58 | IAutoOn.InvokeNotificationMethods( 59 | autoNode, (int)Node.NotificationWMDpiChange 60 | ); 61 | IAutoOn.InvokeNotificationMethods( 62 | autoNode, (int)Node.NotificationVpMouseEnter 63 | ); 64 | IAutoOn.InvokeNotificationMethods( 65 | autoNode, (int)Node.NotificationVpMouseExit 66 | ); 67 | IAutoOn.InvokeNotificationMethods( 68 | autoNode, (int)Node.NotificationOsMemoryWarning 69 | ); 70 | IAutoOn.InvokeNotificationMethods( 71 | autoNode, (int)Node.NotificationTranslationChanged 72 | ); 73 | IAutoOn.InvokeNotificationMethods( 74 | autoNode, (int)Node.NotificationWMAbout 75 | ); 76 | IAutoOn.InvokeNotificationMethods( 77 | autoNode, (int)Node.NotificationCrash 78 | ); 79 | IAutoOn.InvokeNotificationMethods( 80 | autoNode, (int)Node.NotificationOsImeUpdate 81 | ); 82 | IAutoOn.InvokeNotificationMethods( 83 | autoNode, (int)Node.NotificationApplicationResumed 84 | ); 85 | IAutoOn.InvokeNotificationMethods( 86 | autoNode, (int)Node.NotificationApplicationPaused 87 | ); 88 | IAutoOn.InvokeNotificationMethods( 89 | autoNode, (int)Node.NotificationApplicationFocusIn 90 | ); 91 | IAutoOn.InvokeNotificationMethods( 92 | autoNode, (int)Node.NotificationApplicationFocusOut 93 | ); 94 | IAutoOn.InvokeNotificationMethods( 95 | autoNode, (int)Node.NotificationTextServerChanged 96 | ); 97 | IAutoOn.InvokeNotificationMethods( 98 | autoNode, (int)Node.NotificationWMMouseExit 99 | ); 100 | IAutoOn.InvokeNotificationMethods( 101 | autoNode, (int)Node.NotificationWMMouseEnter 102 | ); 103 | IAutoOn.InvokeNotificationMethods( 104 | autoNode, (int)Node.NotificationWMGoBackRequest 105 | ); 106 | IAutoOn.InvokeNotificationMethods( 107 | autoNode, (int)Node.NotificationEditorPreSave 108 | ); 109 | IAutoOn.InvokeNotificationMethods( 110 | autoNode, (int)Node.NotificationExitTree 111 | ); 112 | IAutoOn.InvokeNotificationMethods( 113 | autoNode, (int)Node.NotificationChildOrderChanged 114 | ); 115 | IAutoOn.InvokeNotificationMethods( 116 | autoNode, (int)Node.NotificationReady 117 | ); 118 | IAutoOn.InvokeNotificationMethods( 119 | autoNode, (int)Node.NotificationEditorPostSave 120 | ); 121 | IAutoOn.InvokeNotificationMethods( 122 | autoNode, (int)Node.NotificationUnpaused 123 | ); 124 | IAutoOn.InvokeNotificationMethods( 125 | autoNode, (int)Node.NotificationPhysicsProcess 126 | ); 127 | IAutoOn.InvokeNotificationMethods( 128 | autoNode, (int)Node.NotificationProcess 129 | ); 130 | IAutoOn.InvokeNotificationMethods( 131 | autoNode, (int)Node.NotificationParented 132 | ); 133 | IAutoOn.InvokeNotificationMethods( 134 | autoNode, (int)Node.NotificationUnparented 135 | ); 136 | IAutoOn.InvokeNotificationMethods( 137 | autoNode, (int)Node.NotificationPaused 138 | ); 139 | IAutoOn.InvokeNotificationMethods( 140 | autoNode, (int)Node.NotificationDragBegin 141 | ); 142 | IAutoOn.InvokeNotificationMethods( 143 | autoNode, (int)Node.NotificationDragEnd 144 | ); 145 | IAutoOn.InvokeNotificationMethods( 146 | autoNode, (int)Node.NotificationPathRenamed 147 | ); 148 | IAutoOn.InvokeNotificationMethods( 149 | autoNode, (int)Node.NotificationInternalProcess 150 | ); 151 | IAutoOn.InvokeNotificationMethods( 152 | autoNode, (int)Node.NotificationInternalPhysicsProcess 153 | ); 154 | IAutoOn.InvokeNotificationMethods( 155 | autoNode, (int)Node.NotificationPostEnterTree 156 | ); 157 | IAutoOn.InvokeNotificationMethods( 158 | autoNode, (int)Node.NotificationDisabled 159 | ); 160 | IAutoOn.InvokeNotificationMethods( 161 | autoNode, (int)Node.NotificationEnabled 162 | ); 163 | IAutoOn.InvokeNotificationMethods( 164 | autoNode, (int)Node.NotificationSceneInstantiated 165 | ); 166 | }); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/src/FakeNodeTreeTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using Chickensoft.GodotNodeInterfaces; 6 | using Chickensoft.GoDotTest; 7 | using Godot; 8 | using Moq; 9 | using Shouldly; 10 | 11 | public class FakeNodeTreeTest : TestClass { 12 | public FakeNodeTreeTest(Node testScene) : base(testScene) { 13 | var a = new Mock(); 14 | var b = new Mock(); 15 | var c = new Mock(); 16 | 17 | a.Setup(n => n.Name).Returns("A"); 18 | c.Setup(n => n.Name).Returns("C"); 19 | 20 | A = a.Object; 21 | B = b.Object; 22 | C = c.Object; 23 | } 24 | 25 | public INode A { get; } 26 | public INode B { get; } 27 | public INode C { get; } 28 | 29 | [Test] 30 | public void InitializesAndGetsChildrenAndShowsHasChildren() { 31 | var children = new Dictionary() { ["A"] = A, ["B"] = B }; 32 | var tree = new FakeNodeTree(TestScene, children); 33 | 34 | tree.GetChildren().ShouldBe([A, B]); 35 | tree.HasNode("A").ShouldBeTrue(); 36 | tree.HasNode("B").ShouldBeTrue(); 37 | 38 | tree.GetChildCount().ShouldBe(2); 39 | 40 | tree.GetAllNodes().ShouldBe(new Dictionary() { 41 | ["A"] = A, 42 | ["B"] = B 43 | }); 44 | } 45 | 46 | [Test] 47 | public void InitializesWithNothing() { 48 | var tree = new FakeNodeTree(TestScene); 49 | 50 | tree.GetChildren().ShouldBeEmpty(); 51 | } 52 | 53 | [Test] 54 | public void AddChildWorks() { 55 | var children = new Dictionary() { ["A"] = A, ["B"] = B }; 56 | var tree = new FakeNodeTree(TestScene, children); 57 | 58 | tree.AddChild(C); 59 | tree.GetChildren().ShouldBe([A, B, C]); 60 | tree.HasNode("A").ShouldBeTrue(); 61 | tree.HasNode("B").ShouldBeTrue(); 62 | tree.HasNode("C").ShouldBeTrue(); 63 | 64 | tree.GetChildCount().ShouldBe(3); 65 | } 66 | 67 | [Test] 68 | public void AddChildGeneratesNameForNodeIfNeeded() { 69 | var tree = new FakeNodeTree(TestScene); 70 | tree.AddChild(B); 71 | tree.GetNode(B.GetType().Name + "@0").ShouldBe(B); 72 | } 73 | 74 | [Test] 75 | public void GetNodeReturnsNode() { 76 | var children = new Dictionary() { ["A"] = A, ["B"] = B }; 77 | var tree = new FakeNodeTree(TestScene, children); 78 | 79 | tree.GetNode("A").ShouldBe(A); 80 | tree.GetNode("A").ShouldBe(A); 81 | tree.GetNode("B").ShouldBe(B); 82 | tree.GetNode("nonexistent").ShouldBeNull(); 83 | tree.GetNode("nonexistent").ShouldBeNull(); 84 | } 85 | 86 | [Test] 87 | public void FindChildReturnsMatchingNode() { 88 | var children = new Dictionary() { ["A"] = A, ["B"] = B, ["C"] = C }; 89 | var tree = new FakeNodeTree(TestScene, children); 90 | 91 | var result = tree.FindChild("A"); 92 | result.ShouldBe(A); 93 | } 94 | 95 | [Test] 96 | public void FindChildReturnsNullOnNoMatch() { 97 | var children = new Dictionary() { ["A"] = A, ["B"] = B, ["C"] = C }; 98 | var tree = new FakeNodeTree(TestScene, children); 99 | 100 | var result = tree.FindChild("D"); 101 | result.ShouldBeNull(); 102 | } 103 | 104 | [Test] 105 | public void FindChildrenReturnsMatchingNodes() { 106 | var children = new Dictionary() { ["Apple"] = A, ["Banana"] = B, ["Cherry"] = C }; 107 | var tree = new FakeNodeTree(TestScene, children); 108 | 109 | var results = tree.FindChildren("C*"); 110 | results.ShouldBe([C]); 111 | } 112 | 113 | [Test] 114 | public void GetChildReturnsNodeByIndex() { 115 | var children = new Dictionary() { ["A"] = A, ["B"] = B, ["C"] = C }; 116 | var tree = new FakeNodeTree(TestScene, children); 117 | 118 | var result = tree.GetChild(1); // Get the second child (B). 119 | var result2 = tree.GetChild(1); 120 | result.ShouldBe(B); 121 | result.ShouldBeSameAs(result2); 122 | } 123 | 124 | [Test] 125 | public void GetChildThrowsOnInvalidIndex() { 126 | var tree = new FakeNodeTree(TestScene); 127 | 128 | Should.Throw(() => tree.GetChild(0)); 129 | } 130 | 131 | [Test] 132 | public void GetChildUsesNegativeIndexToGetFromEnd() { 133 | var children = new Dictionary() { ["A"] = A, ["B"] = B, ["C"] = C }; 134 | var tree = new FakeNodeTree(TestScene, children); 135 | 136 | var result = tree.GetChild(-1); 137 | result.ShouldBe(C); 138 | } 139 | 140 | [Test] 141 | public void RemoveChildRemovesNode() { 142 | var children = new Dictionary() { ["A"] = A, ["B"] = B, ["C"] = C }; 143 | var tree = new FakeNodeTree(TestScene, children); 144 | 145 | tree.GetChildCount().ShouldBe(3); 146 | 147 | tree.RemoveChild(B); // Remove the "B" node. 148 | tree.HasNode("B").ShouldBeFalse(); 149 | tree.GetChildren().ShouldBe([A, C]); 150 | 151 | tree.GetChildCount().ShouldBe(2); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/src/MiscTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests; 2 | 3 | using System; 4 | using Chickensoft.AutoInject.Tests.Subjects; 5 | using Chickensoft.GoDotTest; 6 | using Chickensoft.Introspection; 7 | using Godot; 8 | using Shouldly; 9 | 10 | [Meta(typeof(IAutoOn), typeof(IDependent))] 11 | public partial class TestDependent { } 12 | 13 | public class MiscTest(Node testScene) : TestClass(testScene) { 14 | [Test] 15 | public void DependencyPendingCancels() { 16 | var obj = new StringProvider(); 17 | var provider = obj as IBaseProvider; 18 | var initialized = false; 19 | void onInitialized(IBaseProvider provider) => initialized = true; 20 | 21 | provider.ProviderState.OnInitialized += onInitialized; 22 | 23 | var pending = new PendingProvider(provider, onInitialized); 24 | 25 | pending.Unsubscribe(); 26 | 27 | provider.ProviderState.Announce(provider); 28 | 29 | initialized.ShouldBeFalse(); 30 | 31 | obj.QueueFree(); 32 | } 33 | 34 | [Test] 35 | public void ProviderNotFoundException() { 36 | var exception = new ProviderNotFoundException(typeof(ObsoleteAttribute)); 37 | 38 | exception.Message.ShouldContain(nameof(ObsoleteAttribute)); 39 | } 40 | 41 | [Test] 42 | public void IDependentOnResolvedDoesNothing() { 43 | var dependent = new TestDependent(); 44 | 45 | Should.NotThrow(() => ((IDependent)dependent).OnResolved()); 46 | } 47 | 48 | [Test] 49 | public void DefaultProviderState() { 50 | var defaultProvider = new DependencyResolver.DefaultProvider("hi"); 51 | defaultProvider.ProviderState.ShouldNotBeNull(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/src/MultiResolutionTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.GodotGame; 2 | 3 | using System.Threading.Tasks; 4 | using Chickensoft.AutoInject.Tests.Fixtures; 5 | using Chickensoft.GoDotTest; 6 | using Godot; 7 | using GodotTestDriver; 8 | using GodotTestDriver.Util; 9 | using Shouldly; 10 | 11 | public class MultiResolutionTest(Node testScene) : TestClass(testScene) { 12 | private Fixture _fixture = default!; 13 | private MultiProvider _provider = default!; 14 | 15 | [Setup] 16 | public void Setup() { 17 | _fixture = new Fixture(TestScene.GetTree()); 18 | _provider = _fixture.LoadScene( 19 | "res://test/fixtures/MultiProvider.tscn" 20 | ); 21 | } 22 | 23 | [Cleanup] 24 | public void Cleanup() => _fixture.Cleanup(); 25 | 26 | [Test] 27 | public async Task MultiDependentSubscribesToMultiProviderCorrectly() { 28 | await _fixture.AddToRoot(_provider); 29 | await _provider.WaitForEvents(); 30 | _provider.Child.ReadyCalled.ShouldBeTrue(); 31 | _provider.Child.OnResolvedCalled.ShouldBeTrue(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/src/MyNodeTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests; 2 | 3 | using System.Threading.Tasks; 4 | using Chickensoft.GodotNodeInterfaces; 5 | using Chickensoft.GoDotTest; 6 | using Chickensoft.AutoInject.Tests.Fixtures; 7 | using Godot; 8 | using GodotTestDriver; 9 | using Moq; 10 | using Shouldly; 11 | 12 | #pragma warning disable CA1001 13 | public class MyNodeTest(Node testScene) : TestClass(testScene) { 14 | private Fixture _fixture = default!; 15 | private MyNode _scene = default!; 16 | 17 | private Mock _someNode = default!; 18 | private Mock _myUniqueNode = default!; 19 | private Mock _otherUniqueNode = default!; 20 | 21 | [Setup] 22 | public async Task Setup() { 23 | _fixture = new(TestScene.GetTree()); 24 | 25 | _someNode = new(); 26 | _myUniqueNode = new(); 27 | _otherUniqueNode = new(); 28 | 29 | _scene = new MyNode(); 30 | _scene.FakeNodeTree(new() { 31 | ["Path/To/SomeNode"] = _someNode.Object, 32 | ["%MyUniqueNode"] = _myUniqueNode.Object, 33 | ["%OtherUniqueName"] = _otherUniqueNode.Object, 34 | }); 35 | 36 | await _fixture.AddToRoot(_scene); 37 | } 38 | 39 | [Cleanup] 40 | public async Task Cleanup() => await _fixture.Cleanup(); 41 | 42 | [Test] 43 | public void UsesFakeNodeTree() { 44 | // Making a new instance of a node without instantiating a scene doesn't 45 | // trigger NotificationSceneInstantiated, so if we want to make sure our 46 | // AutoNodes get hooked up and use the FakeNodeTree, we need to do it manually. 47 | _scene._Notification((int)Node.NotificationSceneInstantiated); 48 | 49 | _scene.SomeNode.ShouldBe(_someNode.Object); 50 | _scene.MyUniqueNode.ShouldBe(_myUniqueNode.Object); 51 | _scene.DifferentName.ShouldBe(_otherUniqueNode.Object); 52 | _scene._my_unique_node.ShouldBe(_myUniqueNode.Object); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/src/NodeAttributeTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests; 2 | using Chickensoft.GoDotTest; 3 | using Godot; 4 | using Shouldly; 5 | 6 | public class NodeAttributeTest(Node testScene) : TestClass(testScene) { 7 | [Test] 8 | public void Initializes() { 9 | var attr = new NodeAttribute("path"); 10 | attr.Path.ShouldBe("path"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/src/NotificationExtensionsTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests; 2 | using Chickensoft.GoDotTest; 3 | using Godot; 4 | using Shouldly; 5 | 6 | 7 | 8 | public partial class NotificationExtensionsTest( 9 | Node testScene 10 | ) : TestClass(testScene) { 11 | [Test] 12 | public void DoesNothingIfNotIntrospective() { 13 | var node = new Node(); 14 | 15 | Should.NotThrow(() => node.Notify(1)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/src/ResolutionTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.AutoInject.Tests; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | using Chickensoft.AutoInject.Tests.Subjects; 7 | using Chickensoft.GoDotTest; 8 | using Chickensoft.GodotTestDriver; 9 | using Godot; 10 | using Shouldly; 11 | 12 | public class ResolutionTest(Node testScene) : TestClass(testScene) { 13 | [Test] 14 | public void Provides() { 15 | var value = "Hello, world!"; 16 | var provider = new StringProvider() { Value = value }; 17 | 18 | ((IProvide)provider).Value().ShouldBe(value); 19 | 20 | provider._Notification((int)Node.NotificationReady); 21 | 22 | provider.OnProvidedCalled.ShouldBeTrue(); 23 | } 24 | 25 | [Test] 26 | public void ProviderResetsOnTreeExit() { 27 | var value = "Hello, world!"; 28 | var obj = new StringProvider() { Value = value }; 29 | var provider = obj as IBaseProvider; 30 | 31 | ((IProvide)provider).Value().ShouldBe(value); 32 | 33 | obj._Notification((int)Node.NotificationReady); 34 | provider.ProviderState.IsInitialized.ShouldBeTrue(); 35 | 36 | obj._Notification((int)Node.NotificationExitTree); 37 | provider.ProviderState.IsInitialized.ShouldBeFalse(); 38 | } 39 | 40 | [Test] 41 | public void ResolvesDependencyWhenProviderIsAlreadyInitialized() { 42 | var value = "Hello, world!"; 43 | var obj = new StringProvider() { Value = value }; 44 | var provider = obj as IBaseProvider; 45 | var dependent = new StringDependent(); 46 | 47 | obj.AddChild(dependent); 48 | 49 | ((IProvide)provider).Value().ShouldBe(value); 50 | 51 | obj._Notification((int)Node.NotificationReady); 52 | provider.ProviderState.IsInitialized.ShouldBeTrue(); 53 | obj.OnProvidedCalled.ShouldBeTrue(); 54 | 55 | dependent._Notification((int)Node.NotificationReady); 56 | 57 | dependent.OnResolvedCalled.ShouldBeTrue(); 58 | dependent.ResolvedValue.ShouldBe(value); 59 | 60 | obj.RemoveChild(dependent); 61 | dependent.QueueFree(); 62 | obj.QueueFree(); 63 | } 64 | 65 | [Test] 66 | public async Task ResolvesDependencyAfterProviderIsResolved() { 67 | var value = "Hello, world!"; 68 | var obj = new StringProvider() { Value = value }; 69 | var provider = obj as IBaseProvider; 70 | var dependent = new StringDependent(); 71 | var fixture = new Fixture(TestScene.GetTree()); 72 | obj.AddChild(dependent); 73 | 74 | await fixture.AddToRoot(obj); 75 | 76 | ((IProvide)provider).Value().ShouldBe(value); 77 | 78 | provider.ProviderState.IsInitialized.ShouldBeTrue(); 79 | obj.OnProvidedCalled.ShouldBeTrue(); 80 | 81 | dependent.OnResolvedCalled.ShouldBeTrue(); 82 | dependent.ResolvedValue.ShouldBe(value); 83 | ((IDependent)dependent).DependentState.Pending.ShouldBeEmpty(); 84 | 85 | await fixture.Cleanup(); 86 | 87 | obj.RemoveChild(dependent); 88 | dependent.QueueFree(); 89 | obj.QueueFree(); 90 | } 91 | 92 | [Test] 93 | public async Task FindsDependenciesAcrossAncestors() { 94 | var value = "Hello, world!"; 95 | 96 | var objA = new StringProvider() { Value = value }; 97 | var providerA = objA as IBaseProvider; 98 | var objB = new IntProvider() { Value = 10 }; 99 | var providerB = objB as IBaseProvider; 100 | var depObj = new StringDependent(); 101 | var dependent = depObj as IDependent; 102 | var fixture = new Fixture(TestScene.GetTree()); 103 | 104 | objA.AddChild(objB); 105 | objA.AddChild(depObj); 106 | 107 | await fixture.AddToRoot(objA); 108 | 109 | providerA.ProviderState.IsInitialized.ShouldBeTrue(); 110 | objA.OnProvidedCalled.ShouldBeTrue(); 111 | 112 | providerB.ProviderState.IsInitialized.ShouldBeTrue(); 113 | objB.OnProvidedCalled.ShouldBeTrue(); 114 | 115 | depObj.OnResolvedCalled.ShouldBeTrue(); 116 | depObj.ResolvedValue.ShouldBe(value); 117 | dependent.DependentState.Pending.ShouldBeEmpty(); 118 | 119 | objA.RemoveChild(objB); 120 | objB.RemoveChild(depObj); 121 | depObj.QueueFree(); 122 | objB.QueueFree(); 123 | objA.QueueFree(); 124 | } 125 | 126 | [Test] 127 | public void ThrowsWhenNoProviderFound() { 128 | var dependent = new StringDependent(); 129 | 130 | Should.Throw( 131 | () => dependent._Notification((int)Node.NotificationReady) 132 | ); 133 | } 134 | 135 | [Test] 136 | public void UsesReferenceFallbackValueWhenNoProviderFound() { 137 | var fallback = new Resource(); 138 | var dependent = new ReferenceDependentFallback { 139 | FallbackValue = fallback 140 | }; 141 | 142 | dependent._Notification((int)Node.NotificationReady); 143 | 144 | dependent.ResolvedValue.ShouldBe(fallback); 145 | dependent.MyDependency.ShouldBe(fallback); 146 | } 147 | 148 | [Test] 149 | public void DependsOnValueType() { 150 | var value = 10; 151 | var depObj = new IntDependent() { FallbackValue = () => value }; 152 | var dependent = depObj as IDependent; 153 | 154 | depObj._Notification((int)Node.NotificationReady); 155 | 156 | 157 | depObj.OnResolvedCalled.ShouldBeTrue(); 158 | depObj.ResolvedValue.ShouldBe(value); 159 | 160 | depObj._Notification((int)Node.NotificationExitTree); 161 | 162 | dependent.DependentState.Pending.ShouldBeEmpty(); 163 | 164 | depObj.QueueFree(); 165 | } 166 | 167 | [Test] 168 | public void ThrowsIfFallbackProducesNullAfterPreviousValueIsGarbageCollected( 169 | ) { 170 | var currentFallback = 0; 171 | var replacementValue = new object(); 172 | var fallbacks = new List() { new(), null, replacementValue }; 173 | 174 | var value = Utils.CreateWeakReference(); 175 | 176 | // Fallback will be called 3 times in this test. First will be non-null, 177 | // second will be null, third will be non-null and different from the first. 178 | var depObj = new WeakReferenceDependent() { 179 | Fallback = () => fallbacks[currentFallback++]! 180 | }; 181 | 182 | var dependent = depObj as IDependent; 183 | 184 | depObj._Notification((int)Node.NotificationReady); 185 | 186 | // Let's access the fallback value to ensure the default provider is setup. 187 | depObj.MyDependency.ShouldNotBeNull(); 188 | 189 | // Simulate a garbage collected object. We support weak references to 190 | // dependencies to avoid causing build issues when reloading the scene. 191 | Utils.ClearWeakReference(value); 192 | 193 | // To test this highly specific scenario, we have to clear ALL 194 | // weak references to the object, including the one in the default provider 195 | // that's generated behind-the-scenes for us. 196 | 197 | // Let's dig out the weak ref used in the default provider from the 198 | // dependent's internal state... 199 | var underlyingDefaultProvider = 200 | (DependencyResolver.DefaultProvider) 201 | depObj.MixinState.Get().Dependencies[typeof(object)]; 202 | 203 | var actualWeakRef = (WeakReference)underlyingDefaultProvider._value; 204 | 205 | Utils.ClearWeakReference(actualWeakRef); 206 | 207 | var e = Should.Throw( 208 | () => depObj.MyDependency 209 | ); 210 | 211 | e.Message.ShouldContain("cannot create a null value"); 212 | 213 | // Now that the fallback returns a valid value, the dependency should 214 | // be resolved once again. 215 | depObj.MyDependency.ShouldBeSameAs(replacementValue); 216 | } 217 | 218 | [Test] 219 | public void ThrowsOnDependencyTableThatWasTamperedWith() { 220 | var fallback = "Hello, world!"; 221 | var depObj = new StringDependentFallback { 222 | FallbackValue = fallback 223 | }; 224 | var dependent = depObj as IDependent; 225 | 226 | depObj._Notification((int)Node.NotificationReady); 227 | 228 | dependent.DependentState.Dependencies[typeof(string)] = new BadProvider(); 229 | 230 | Should.Throw( 231 | () => depObj.MyDependency.ShouldBe(fallback) 232 | ); 233 | } 234 | 235 | [Test] 236 | public void DependentCancelsPendingIfRemovedFromTree() { 237 | var provider = new StringProvider(); 238 | var depObj = new StringDependent(); 239 | var dependent = depObj as IDependent; 240 | 241 | provider.AddChild(depObj); 242 | 243 | depObj._Notification((int)Node.NotificationReady); 244 | 245 | dependent.DependentState.Pending.ShouldNotBeEmpty(); 246 | 247 | depObj._Notification((int)Node.NotificationExitTree); 248 | 249 | dependent.DependentState.Pending.ShouldBeEmpty(); 250 | 251 | provider.RemoveChild(depObj); 252 | depObj.QueueFree(); 253 | provider.QueueFree(); 254 | } 255 | 256 | [Test] 257 | public void AccessingDependencyBeforeProvidedEvenIfCreatedThrows() { 258 | // Accessing a dependency that might already be available (but the provider 259 | // hasn't called Provide() yet) should throw an exception. 260 | 261 | var provider = new StringProvider(); 262 | var dependent = new StringDependent(); 263 | 264 | provider.AddChild(dependent); 265 | 266 | dependent._Notification((int)Node.NotificationReady); 267 | Should.Throw(() => dependent.MyDependency); 268 | } 269 | 270 | [Test] 271 | public void DependentWithNoDependenciesHasOnResolvedCalled() { 272 | var provider = new StringProvider(); 273 | var dependent = new NoDependenciesDependent(); 274 | 275 | provider.AddChild(dependent); 276 | 277 | dependent._Notification((int)Node.NotificationReady); 278 | 279 | dependent.OnResolvedCalled.ShouldBeTrue(); 280 | } 281 | 282 | [Test] 283 | public void FakesDependency() { 284 | var dependent = new FakedDependent(); 285 | 286 | var fakeValue = "I'm fake!"; 287 | dependent.FakeDependency(fakeValue); 288 | 289 | TestScene.AddChild(dependent); 290 | 291 | dependent._Notification((int)Node.NotificationReady); 292 | 293 | dependent.OnResolvedCalled.ShouldBeTrue(); 294 | dependent.MyDependency.ShouldBe(fakeValue); 295 | 296 | TestScene.RemoveChild(dependent); 297 | } 298 | 299 | public class BadProvider : IBaseProvider { 300 | public ProviderState ProviderState { get; } 301 | 302 | public BadProvider() { 303 | ProviderState = new ProviderState { 304 | IsInitialized = true 305 | }; 306 | } 307 | } 308 | 309 | public static class Utils { 310 | public static WeakReference CreateWeakReference() => new(new object()); 311 | 312 | public static void ClearWeakReference(WeakReference weakReference) { 313 | weakReference.Target = null; 314 | 315 | while (weakReference.Target is not null) { 316 | GC.Collect(); 317 | GC.WaitForPendingFinalizers(); 318 | } 319 | } 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.Tests/test/src/SuperNodeTest.cs: -------------------------------------------------------------------------------- 1 | namespace Chickensoft.GodotGame; 2 | 3 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chickensoft.AutoInject", "Chickensoft.AutoInject\Chickensoft.AutoInject.csproj", "{9F40CD34-5F3F-4431-90FE-C3577FC15AC4}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chickensoft.AutoInject.Tests", "Chickensoft.AutoInject.Tests\Chickensoft.AutoInject.Tests.csproj", "{9DD3D030-C9BE-42FE-B56F-8BC5FCF9579E}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chickensoft.AutoInject.Analyzers", "Chickensoft.AutoInject.Analyzers\Chickensoft.AutoInject.Analyzers.csproj", "{70A5327C-5874-428E-BF51-C6854A1608F5}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chickensoft.AutoInject.Analyzers.Tests", "Chickensoft.AutoInject.Analyzers.Tests\Chickensoft.AutoInject.Analyzers.Tests.csproj", "{AF663183-3114-406E-8A2E-8535132FEBB7}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {9F40CD34-5F3F-4431-90FE-C3577FC15AC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {9F40CD34-5F3F-4431-90FE-C3577FC15AC4}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {9F40CD34-5F3F-4431-90FE-C3577FC15AC4}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {9F40CD34-5F3F-4431-90FE-C3577FC15AC4}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {9DD3D030-C9BE-42FE-B56F-8BC5FCF9579E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {9DD3D030-C9BE-42FE-B56F-8BC5FCF9579E}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {9DD3D030-C9BE-42FE-B56F-8BC5FCF9579E}.Release|Any CPU.ActiveCfg = Debug|Any CPU 30 | {9DD3D030-C9BE-42FE-B56F-8BC5FCF9579E}.Release|Any CPU.Build.0 = Debug|Any CPU 31 | {70A5327C-5874-428E-BF51-C6854A1608F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {70A5327C-5874-428E-BF51-C6854A1608F5}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {70A5327C-5874-428E-BF51-C6854A1608F5}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {70A5327C-5874-428E-BF51-C6854A1608F5}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {AF663183-3114-406E-8A2E-8535132FEBB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {AF663183-3114-406E-8A2E-8535132FEBB7}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {AF663183-3114-406E-8A2E-8535132FEBB7}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {AF663183-3114-406E-8A2E-8535132FEBB7}.Release|Any CPU.Build.0 = Release|Any CPU 39 | EndGlobalSection 40 | EndGlobal 41 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject/Chickensoft.AutoInject.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | net8.0 6 | true 7 | preview 8 | enable 9 | ./nupkg 10 | 11 | true 12 | false 13 | contentFiles 14 | true 15 | false 16 | false 17 | CS8021 18 | true 19 | true 20 | true 21 | 22 | AutoInject 23 | 0.0.0-devbuild 24 | Node-based dependency injection for C# Godot scripts — without reflection! 25 | © 2023 Chickensoft 26 | Chickensoft 27 | Chickensoft 28 | 29 | Chickensoft.AutoInject 30 | Chickensoft.AutoInject release. 31 | icon.png 32 | dependency injection; di; godot; chickensoft; nodes 33 | README.md 34 | LICENSE 35 | https://github.com/chickensoft-games/AutoInject 36 | 37 | git 38 | https://github.com/chickensoft-games/AutoInject 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | true 53 | $(ContentTargetFolders)\cs\any\$(PackageId)%(RecursiveDir) 54 | 55 | 56 | true 57 | $(ContentTargetFolders)\any\any\$(PackageId)\%(RecursiveDir)\ 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject/icon.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:8b97072e83ed4405d750903e9c8b5c5e57e58993b4187149f193af2f55cdfc9a 3 | size 216589 4 | -------------------------------------------------------------------------------- /Chickensoft.AutoInject/nupkg/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chickensoft-games/AutoInject/9aeb73934a2473c13659d9baafb5adb65b07d688/Chickensoft.AutoInject/nupkg/.gitkeep -------------------------------------------------------------------------------- /Chickensoft.AutoInject/src/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chickensoft-games/AutoInject/9aeb73934a2473c13659d9baafb5adb65b07d688/Chickensoft.AutoInject/src/.gitkeep -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2023 Chickensoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "**/*.*" 4 | ], 5 | "ignorePaths": [ 6 | "**/*.tscn", 7 | "**/*.import", 8 | "Chickensoft.AutoInject.Tests/badges/**/*.*", 9 | "Chickensoft.AutoInject.Tests/coverage/**/*.*", 10 | "Chickensoft.AutoInject.Tests/.godot/**/*.*", 11 | "**/obj/**/*.*", 12 | "**/bin/**/*.*", 13 | "Chickensoft.AutoInject/nupkg/**/*.*" 14 | ], 15 | "words": [ 16 | "assemblyfilters", 17 | "automerge", 18 | "branchcoverage", 19 | "brandedoutcast", 20 | "buildtransitive", 21 | "camelcase", 22 | "chickenpackage", 23 | "Chickensoft", 24 | "classfilters", 25 | "contentfiles", 26 | "CYGWIN", 27 | "devbuild", 28 | "endregion", 29 | "Finalizers", 30 | "globaltool", 31 | "godotengine", 32 | "godotpackage", 33 | "issuecomment", 34 | "lcov", 35 | "linecoverage", 36 | "Metatype", 37 | "methodcoverage", 38 | "missingall", 39 | "mktemp", 40 | "msbuild", 41 | "MSYS", 42 | "nameof", 43 | "netstandard", 44 | "NOLOGO", 45 | "nupkg", 46 | "Omnisharp", 47 | "onready", 48 | "opencover", 49 | "OPTOUT", 50 | "paramref", 51 | "pascalcase", 52 | "Postinitialize", 53 | "Predelete", 54 | "renovatebot", 55 | "reportgenerator", 56 | "reporttypes", 57 | "Shouldly", 58 | "skipautoprops", 59 | "subfolders", 60 | "targetargs", 61 | "targetdir", 62 | "tscn", 63 | "typeof", 64 | "typeparam", 65 | "typeparamref", 66 | "ulong", 67 | "Unparented", 68 | "warnaserror", 69 | "Xunit" 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /docs/renovatebot_pr.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:b63d75d932457b93f79f54e5c4192d201a29aefb4e437a3732c63099ec8f78a1 3 | size 148110 4 | -------------------------------------------------------------------------------- /docs/spelling_fix.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:870bad263d4bbf803f2278af6e4346cf1e9c07c48c51d85c40ad9b5b63f761b2 3 | size 71136 4 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.411", 4 | "rollForward": "latestMinor" 5 | }, 6 | "msbuild-sdks": { 7 | "Godot.NET.Sdk": "4.4.1" 8 | } 9 | } -------------------------------------------------------------------------------- /manual_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copy source files from Chickensoft.AutoInject.Tests/src/**/*.cs 4 | # to Chickensoft.AutoInject/src/**/*.cs 5 | # 6 | # Because source-only packages are hard to develop and test, we 7 | # actually keep the source that goes in the source-only package inside 8 | # the test project to make it easier to develop and test. 9 | # 10 | # we can always copy it right before publishing the package. 11 | 12 | mkdir -p Chickensoft.AutoInject/src 13 | cp -v -r Chickensoft.AutoInject.Tests/src/* Chickensoft.AutoInject/src/ 14 | # Define the multiline prefix and suffix 15 | PREFIX="#pragma warning disable 16 | #nullable enable 17 | " 18 | SUFFIX=" 19 | #nullable restore 20 | #pragma warning restore" 21 | 22 | # Function to add prefix and suffix to a file 23 | add_prefix_suffix() { 24 | local file="$1" 25 | # Create a temporary file 26 | tmp_file=$(mktemp) 27 | 28 | # Add prefix, content of the file, and suffix to the temporary file 29 | { 30 | echo "$PREFIX" 31 | cat "$file" 32 | echo "$SUFFIX" 33 | } > "$tmp_file" 34 | 35 | # Move the temporary file to the original file 36 | mv "$tmp_file" "$file" 37 | } 38 | 39 | # Export the function and variables so they can be used by find 40 | export -f add_prefix_suffix 41 | export PREFIX 42 | export SUFFIX 43 | 44 | # Find all files and apply the function 45 | find Chickensoft.AutoInject/src -type f -name "*.cs" -exec bash -c 'add_prefix_suffix "$0"' {} \; 46 | 47 | cd Chickensoft.AutoInject 48 | dotnet build -c Release 49 | 50 | # Delete everything copied into Chickensoft.AutoInject/src 51 | rm -r src 52 | 53 | # Recreate folder and .gitkeep file 54 | mkdir src 55 | touch src/.gitkeep 56 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>chickensoft-games/renovate:godot"] 4 | } 5 | --------------------------------------------------------------------------------