├── .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 |
--------------------------------------------------------------------------------
/Chickensoft.AutoInject.Tests/badges/line_coverage.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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