mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-17 08:14:54 +00:00
Add Unreal Engine 5 specific ignores, raises, docs and tests for C/C++ (#1564)
This commit is contained in:
1 parent
5d44d017c8
commit
3975a91bfe
54 files changed
+1193
-12
No files matched your search
@@ -24,6 +24,14 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
`activate_project`/project lookup, raising `FileNotFoundError` in `RegisteredProject.matches_root_path`
|
||||
|
||||
* Language Servers:
|
||||
- C/C++ (clangd): add Unreal Engine 5 fixture and tests verifying that reflection-macro
|
||||
code (`UCLASS`, `UFUNCTION`, `UPROPERTY`, `GENERATED_BODY`) yields correct symbols,
|
||||
references, definitions, and rename edits in hand-written sources (never in
|
||||
UHT-generated files); add an Unreal Engine setup guide.
|
||||
- C/C++ (clangd, ccls): detect Unreal Engine projects by a `.uproject` file at the project
|
||||
root and skip the engine build/cache directories (`Binaries`, `DerivedDataCache`,
|
||||
`Intermediate`, `Saved`) during indexing. clangd fails with a pointer to the Unreal Engine
|
||||
setup guide when started in such a project without a `compile_commands.json`. #1566
|
||||
- `typescript_vts`: Add `initialization_options` setting in `ls_specific_settings.typescript_vts`.
|
||||
The dict is forwarded to vtsls via `initializationOptions`, `workspace/didChangeConfiguration`,
|
||||
and `workspace/configuration` pulls. Enables Yarn PnP setups with `typescript.tsdk` pointing
|
||||
|
||||
@@ -55,7 +55,8 @@ Some languages require additional installations or setup steps, as noted.
|
||||
* **C/C++**
|
||||
(by default, uses the clangd language server (language `cpp`) but we also support ccls (language `cpp_ccls`);
|
||||
for best results, provide a `compile_commands.json` at the repository root;
|
||||
see the [C/C++ Setup Guide](../03-special-guides/cpp_setup) for details.)
|
||||
see the [C/C++ Setup Guide](../03-special-guides/cpp_setup) for details;
|
||||
for Unreal Engine 5 projects, see the [Unreal Engine Setup Guide](../03-special-guides/unreal_engine_setup_guide_for_serena).)
|
||||
* **Clojure**
|
||||
* **Crystal**
|
||||
(requires [Crystalline](https://github.com/elbywan/crystalline) language server to be installed and available on PATH;
|
||||
|
||||
@@ -100,3 +100,8 @@ the language server is restarted.
|
||||
- Clangd official documentation: https://clangd.llvm.org/
|
||||
- Clangd project setup: https://clangd.llvm.org/installation#project-setup
|
||||
- CCLS repository: https://github.com/MaskRay/ccls
|
||||
|
||||
## Unreal Engine projects
|
||||
|
||||
For Unreal Engine 5 projects (reflection macros, UnrealBuildTool), see the
|
||||
[Unreal Engine Setup Guide](unreal_engine_setup_guide_for_serena.md).
|
||||
@@ -0,0 +1,103 @@
|
||||
# Unreal Engine Setup Guide
|
||||
|
||||
This guide explains how to prepare an Unreal Engine 5 C++ project so that Serena's
|
||||
clangd-based C/C++ support can provide full code intelligence: symbol search,
|
||||
cross-file references, and symbol-level editing in your hand-written sources.
|
||||
|
||||
UE game code uses a macro-based reflection layer (`UCLASS`, `UFUNCTION`, `UPROPERTY`,
|
||||
`GENERATED_BODY`) and engine types (`TArray`, `TMap`). clangd handles all of this,
|
||||
provided it receives the compiler flags for your project via a `compile_commands.json`
|
||||
at the project root. Unreal's build system (UnrealBuildTool) does not produce this
|
||||
file by default; this guide shows how to obtain it.
|
||||
|
||||
---
|
||||
## Prerequisites
|
||||
|
||||
- An Unreal Engine 5 C++ project that has been **built at least once** (the build
|
||||
generates the `*.generated.h` headers that your sources include).
|
||||
- No additional language server: Serena downloads clangd automatically.
|
||||
- clangd never compiles your code. The compilation database is only a list of flags.
|
||||
|
||||
---
|
||||
## Getting a compilation database
|
||||
|
||||
If clangd starts in a project that has a `.uproject` file but no `compile_commands.json`,
|
||||
Serena fails the language server startup with an error that points back to this guide,
|
||||
because clangd cannot resolve engine headers or reflection macros without the database.
|
||||
Pick one of the following routes to create it.
|
||||
|
||||
### Route 1: VSCode project files (no extra installs)
|
||||
|
||||
UnrealBuildTool's VSCode project generator emits per-project compile commands.
|
||||
Run it from your engine installation (VSCode itself is not required):
|
||||
|
||||
<Engine>\Build\BatchFiles\Build.bat -projectfiles -project="<YourProject>.uproject" -game -VSCode
|
||||
|
||||
This produces `.vscode/compileCommands_<YourProject>.json` inside your project.
|
||||
Copy or symlink it to the project root as `compile_commands.json`.
|
||||
|
||||
If you already use VSCode with UE, the file likely exists; the editor's
|
||||
"Tools > Refresh Visual Studio Code Project" action maintains it.
|
||||
|
||||
### Route 2: UnrealBuildTool's clang database mode (requires LLVM installed)
|
||||
|
||||
<Engine>\Binaries\DotNET\UnrealBuildTool\UnrealBuildTool.exe -mode=GenerateClangDatabase -project="<YourProject>.uproject" <YourProject>Editor Win64 Development -OutputDir="<YourProject's directory>"
|
||||
|
||||
This emits clang-native commands (cleanest flags for clangd) but requires a Clang
|
||||
toolchain installed on Windows.
|
||||
|
||||
---
|
||||
## Build artifact directories
|
||||
|
||||
Generated reflection code (`*.gen.cpp`, `*.generated.h`) legitimately references your
|
||||
functions, so symbol results could otherwise include hits inside `Intermediate/`. When a
|
||||
`.uproject` file is present at the project root, the clangd and ccls language servers skip
|
||||
UE's build and cache directories (`Binaries`, `DerivedDataCache`, `Intermediate`, `Saved`)
|
||||
during indexing. This is automatic and needs no configuration.
|
||||
|
||||
To exclude further paths, add them to `ignored_paths` in your project's
|
||||
`.serena/project.yml`.
|
||||
|
||||
---
|
||||
## Known behavior
|
||||
|
||||
- **`GENERATED_BODY()` and `__LINE__`:** the macro expands using its line number.
|
||||
After editing lines above it, clangd may report stale-macro diagnostics until the
|
||||
next build regenerates headers. Symbol operations keep working, since clangd is
|
||||
designed to operate on code with errors.
|
||||
- **First index:** large projects take a few minutes to index once; afterwards
|
||||
results are incremental. The index cache is kept under `.serena/.cache` inside
|
||||
the project.
|
||||
- **New `UFUNCTION`/`UCLASS` declarations** need a build before their generated
|
||||
headers exist.
|
||||
- **Symbol searches on large projects:** prefer passing `relative_path` to
|
||||
`find_symbol`. An unscoped search visits every translation unit, and UE
|
||||
files are expensive to parse because each pulls in large engine headers.
|
||||
|
||||
---
|
||||
## Troubleshooting
|
||||
|
||||
Extra flags are easiest to add via a `.clangd` file at the project root, e.g.:
|
||||
|
||||
CompileFlags:
|
||||
Add: [-D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH, -ferror-limit=200]
|
||||
|
||||
- **`STL1000: Unexpected compiler version` errors:** recent MSVC STL headers
|
||||
assert a minimum Clang version that may be newer than Serena's bundled clangd.
|
||||
Defining `_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH` (see above) silences the
|
||||
check; clangd only parses, so the mismatch is harmless.
|
||||
- **Truncated symbol trees / symbols missing below a certain line:** clangd
|
||||
aborts a file's parse after ~20 errors by default, which discards everything
|
||||
declared after that point. Raising the limit with `-ferror-limit=200` keeps
|
||||
the symbol tree intact even when diagnostics are noisy (common right after
|
||||
edits, before the next UE build regenerates headers).
|
||||
- **Stale results after changing the compilation database:** clangd's index
|
||||
shards in `.serena/.cache` were built with the old flags. Delete that cache
|
||||
directory and let the project re-index.
|
||||
|
||||
---
|
||||
## Verifying the setup
|
||||
|
||||
After activating the project in Serena, a symbol overview of any `UCLASS` header
|
||||
should list the class with its `UFUNCTION` methods and `UPROPERTY` fields, and
|
||||
references to a method should resolve to your `Source/` files only.
|
||||
+1
-1
@@ -368,4 +368,4 @@ markers = [
|
||||
skip = '.git*,*.svg,*.lock,*.min.*,*test_memories_manager.py'
|
||||
check-hidden = true
|
||||
ignore-regex = '\.\w+'
|
||||
ignore-words-list = 'paket,EDN,als'
|
||||
ignore-words-list = 'paket,EDN,als,ue'
|
||||
@@ -36,6 +36,7 @@ from typing import Any, cast
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.language_servers.common import UE_IGNORED_DIRNAMES, is_unreal_engine_project
|
||||
from solidlsp.ls import (
|
||||
LanguageServerDependencyProvider,
|
||||
LanguageServerDependencyProviderSinglePath,
|
||||
@@ -71,10 +72,11 @@ class CCLS(SolidLanguageServer):
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
ignored_dirs = [
|
||||
".ccls-cache",
|
||||
]
|
||||
return super().is_ignored_dirname(dirname) or dirname in ignored_dirs
|
||||
return (
|
||||
super().is_ignored_dirname(dirname)
|
||||
or dirname == ".ccls-cache"
|
||||
or (is_unreal_engine_project(self.repository_root_path) and dirname in UE_IGNORED_DIRNAMES)
|
||||
)
|
||||
|
||||
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
|
||||
def _get_or_install_core_dependency(self) -> str:
|
||||
|
||||
@@ -9,12 +9,14 @@ from typing import Any, cast
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.language_servers.common import UE_IGNORED_DIRNAMES
|
||||
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, ProcessLaunchInfo, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_exceptions import SolidLSPException
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
from .common import RuntimeDependency, RuntimeDependencyCollection
|
||||
from .common import RuntimeDependency, RuntimeDependencyCollection, is_unreal_engine_project
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -103,10 +105,11 @@ class ClangdLanguageServer(SolidLanguageServer):
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
ignored_dirs = [
|
||||
".ccls-cache",
|
||||
]
|
||||
return super().is_ignored_dirname(dirname) or dirname in ignored_dirs
|
||||
return (
|
||||
super().is_ignored_dirname(dirname)
|
||||
or dirname == ".ccls-cache"
|
||||
or (is_unreal_engine_project(self.repository_root_path) and dirname in UE_IGNORED_DIRNAMES)
|
||||
)
|
||||
|
||||
def _prepare_compile_commands(self) -> str | None:
|
||||
"""
|
||||
@@ -128,7 +131,14 @@ class ClangdLanguageServer(SolidLanguageServer):
|
||||
compile_db_path = os.path.join(self.repository_root_path, "compile_commands.json")
|
||||
|
||||
if not os.path.exists(compile_db_path):
|
||||
# No compile_commands.json, nothing to do
|
||||
# clangd can't resolve UE engine headers without the database; fail with guidance.
|
||||
if is_unreal_engine_project(self.repository_root_path):
|
||||
raise SolidLSPException(
|
||||
f"No compile_commands.json found at {self.repository_root_path}, but this looks like an "
|
||||
"Unreal Engine project (.uproject present). clangd needs a compilation database to resolve "
|
||||
"engine headers and macros. See docs/03-special-guides/"
|
||||
"unreal_engine_setup_guide_for_serena.md to generate one."
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -216,3 +217,13 @@ def quote_windows_path(path: str) -> str:
|
||||
return path
|
||||
return f'"{path}"'
|
||||
return path
|
||||
|
||||
|
||||
UE_IGNORED_DIRNAMES = frozenset({"Binaries", "DerivedDataCache", "Intermediate", "Saved"})
|
||||
"""Unreal Engine build and cache directories. Matched per directory name, so per-plugin and
|
||||
per-module copies (e.g. ``Plugins/Foo/Intermediate``) are covered too."""
|
||||
|
||||
|
||||
def is_unreal_engine_project(repository_root_path: str) -> bool:
|
||||
""":return: whether the repository root contains an Unreal Engine ``.uproject`` file."""
|
||||
return any(pathlib.Path(repository_root_path).glob("*.uproject"))
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/*===========================================================================
|
||||
Generated code exported from UnrealHeaderTool.
|
||||
DO NOT modify this manually! Edit the corresponding .h files instead!
|
||||
===========================================================================*/
|
||||
// Test stub: trimmed to what the fixture needs.
|
||||
// DECLARE_FUNCTION(execOnAbilityInput)
|
||||
#pragma once
|
||||
|
||||
class AAbilityActor;
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*===========================================================================
|
||||
Generated code exported from UnrealHeaderTool.
|
||||
DO NOT modify this manually! Edit the corresponding .h files instead!
|
||||
===========================================================================*/
|
||||
// Test stub: repeats the hand-written identifiers (as real thunks do) so a
|
||||
// text search would surface this file; symbol-level tools must not.
|
||||
//
|
||||
// void UAbilityComponent::execTriggerAbility(...) { TriggerAbility(AbilityName); }
|
||||
// void UAbilityComponent::execGetRemainingCooldown(...) { GetRemainingCooldown(AbilityName); }
|
||||
// void AAbilityActor::execOnAbilityInput(...) { OnAbilityInput(AbilityName); }
|
||||
|
||||
namespace UnrealReflectionStub
|
||||
{
|
||||
const char* GeneratedSymbols[] = {
|
||||
"UAbilityComponent",
|
||||
"TriggerAbility",
|
||||
"GetRemainingCooldown",
|
||||
"AAbilityActor",
|
||||
"OnAbilityInput",
|
||||
"FAbilityInfo",
|
||||
};
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/*===========================================================================
|
||||
Generated code exported from UnrealHeaderTool.
|
||||
DO NOT modify this manually! Edit the corresponding .h files instead!
|
||||
===========================================================================*/
|
||||
// Test stub: trimmed to what the fixture needs.
|
||||
// DECLARE_FUNCTION(execTriggerAbility)
|
||||
// DECLARE_FUNCTION(execGetRemainingCooldown)
|
||||
#pragma once
|
||||
|
||||
class UAbilityComponent;
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/*===========================================================================
|
||||
Generated code exported from UnrealHeaderTool.
|
||||
DO NOT modify this manually! Edit the corresponding .h files instead!
|
||||
===========================================================================*/
|
||||
// Test stub: trimmed to what the fixture needs.
|
||||
#pragma once
|
||||
|
||||
struct FAbilityInfo;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/*===========================================================================
|
||||
Generated code exported from UnrealHeaderTool.
|
||||
DO NOT modify this manually! Edit the corresponding .h files instead!
|
||||
===========================================================================*/
|
||||
// Test stub: trimmed to what the fixture needs.
|
||||
// DECLARE_FUNCTION(execReceiveDamage)
|
||||
#pragma once
|
||||
|
||||
class UDamageable;
|
||||
class IDamageable;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/*===========================================================================
|
||||
Generated code exported from UnrealHeaderTool.
|
||||
DO NOT modify this manually! Edit the corresponding .h files instead!
|
||||
===========================================================================*/
|
||||
// Test stub: trimmed to what the fixture needs.
|
||||
// DECLARE_FUNCTION(execHeal)
|
||||
// DECLARE_FUNCTION(execReceiveDamage)
|
||||
#pragma once
|
||||
|
||||
class AGameCharacter;
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"FileVersion": 3,
|
||||
"EngineAssociation": "5.7"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#include "AbilityActor.h"
|
||||
|
||||
void AAbilityActor::BeginPlay()
|
||||
{
|
||||
AActor::BeginPlay();
|
||||
if (AbilityComponent)
|
||||
{
|
||||
AbilityComponent->TriggerAbility(FName("Dash"));
|
||||
}
|
||||
}
|
||||
|
||||
void AAbilityActor::OnAbilityInput(const FName& AbilityName)
|
||||
{
|
||||
if (AbilityComponent)
|
||||
{
|
||||
AbilityComponent->TriggerAbility(AbilityName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "AbilityComponent.h"
|
||||
#include "AbilityActor.generated.h"
|
||||
|
||||
UCLASS(Blueprintable)
|
||||
class TESTGAME_API AAbilityActor : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Input")
|
||||
void OnAbilityInput(const FName& AbilityName);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Components")
|
||||
TObjectPtr<UAbilityComponent> AbilityComponent = nullptr;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Abilities")
|
||||
TSubclassOf<UAbilityComponent> ComponentClass;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
#include "AbilityComponent.h"
|
||||
|
||||
void UAbilityComponent::TriggerAbility(const FName& AbilityName)
|
||||
{
|
||||
if (!ActiveCooldowns.Contains(AbilityName))
|
||||
{
|
||||
ActiveCooldowns.Add(AbilityName, 1.0f);
|
||||
}
|
||||
State = EAbilityState::Active;
|
||||
OnAbilityTriggered.Broadcast(AbilityName);
|
||||
}
|
||||
|
||||
float UAbilityComponent::GetRemainingCooldown(const FName& AbilityName) const
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "AbilityTypes.h"
|
||||
#include "AbilityComponent.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAbilityTriggered, const FName&, AbilityName);
|
||||
DECLARE_MULTICAST_DELEGATE_OneParam(FOnCooldownExpired, const FName&);
|
||||
|
||||
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
|
||||
class TESTGAME_API UAbilityComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Abilities")
|
||||
void TriggerAbility(const FName& AbilityName);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Abilities")
|
||||
float GetRemainingCooldown(const FName& AbilityName) const;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Abilities")
|
||||
TArray<FAbilityInfo> Abilities;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, Category = "Abilities")
|
||||
TMap<FName, float> ActiveCooldowns;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Abilities")
|
||||
FOnAbilityTriggered OnAbilityTriggered;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category = "Abilities")
|
||||
EAbilityState State = EAbilityState::Idle;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category = "Abilities")
|
||||
TSet<FName> UnlockedAbilities;
|
||||
|
||||
// Non-dynamic delegates are not reflected and cannot be UPROPERTYs.
|
||||
FOnCooldownExpired OnCooldownExpired;
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "AbilityTypes.generated.h"
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EAbilityState : uint8
|
||||
{
|
||||
Idle,
|
||||
Active UMETA(DisplayName = "Active (in use)"),
|
||||
Cooldown,
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct TESTGAME_API FAbilityInfo
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FName AbilityName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
float CooldownSeconds = 1.0f;
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "Damageable.generated.h"
|
||||
|
||||
UINTERFACE(MinimalAPI, Blueprintable)
|
||||
class UDamageable : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
class TESTGAME_API IDamageable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Damage")
|
||||
virtual void ReceiveDamage(float Amount) = 0;
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
#include "GameCharacter.h"
|
||||
#include "TestGameLog.h"
|
||||
|
||||
AGameCharacter::AGameCharacter()
|
||||
{
|
||||
Abilities = CreateDefaultSubobject<UAbilityComponent>(FName(TEXT("Abilities")));
|
||||
}
|
||||
|
||||
void AGameCharacter::ReceiveDamage(float Amount)
|
||||
{
|
||||
check(Amount >= 0.0f);
|
||||
Health -= Amount;
|
||||
LastDamageAmount = Amount;
|
||||
UE_LOG(LogTestGame, Warning, TEXT("Received %f damage"), Amount);
|
||||
|
||||
if (AActor* Target = CurrentTarget.Get())
|
||||
{
|
||||
if (AGameCharacter* OtherCharacter = Cast<AGameCharacter>(Target))
|
||||
{
|
||||
OtherCharacter->Heal(Amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AGameCharacter::Heal(float& Amount)
|
||||
{
|
||||
Health += Amount;
|
||||
Amount = 0.0f;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "AbilityComponent.h"
|
||||
#include "Damageable.h"
|
||||
#include "GameCharacter.generated.h"
|
||||
|
||||
UCLASS(Blueprintable)
|
||||
class TESTGAME_API AGameCharacter : public ACharacter, public IDamageable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AGameCharacter();
|
||||
|
||||
virtual void ReceiveDamage(float Amount) override;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Health")
|
||||
void Heal(UPARAM(ref) float& Amount);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawn")
|
||||
FVector SpawnOffset = FVector(0.0, 0.0, 100.0);
|
||||
|
||||
UPROPERTY(EditAnywhere, Category = "Abilities")
|
||||
TObjectPtr<UAbilityComponent> Abilities;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, Category = "Targeting")
|
||||
TWeakObjectPtr<AActor> CurrentTarget;
|
||||
|
||||
UPROPERTY(EditAnywhere, Category = "Loadout")
|
||||
TSoftObjectPtr<UAbilityComponent> FallbackLoadout;
|
||||
|
||||
private:
|
||||
// Non-reflected runtime state: smart pointers and optionals are not UPROPERTYs.
|
||||
TSharedPtr<FAbilityInfo> PendingAbility;
|
||||
TOptional<float> LastDamageAmount;
|
||||
float Health = 100.0f;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogTestGame, Log, All);
|
||||
@@ -0,0 +1,6 @@
|
||||
#include "TestGameLog.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogTestGame)
|
||||
|
||||
IMPLEMENT_PRIMARY_GAME_MODULE(FDefaultGameModuleImpl, TestGame, "TestGame");
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "UObject/Object.h"
|
||||
|
||||
class UActorComponent : public UObject
|
||||
{
|
||||
public:
|
||||
virtual void BeginPlay() {}
|
||||
virtual void TickComponent(float DeltaTime) {}
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
// Minimal TArray stand-in: just enough surface for the fixture sources to parse.
|
||||
#pragma once
|
||||
|
||||
template <typename ElementType>
|
||||
class TArray
|
||||
{
|
||||
public:
|
||||
void Add(const ElementType& Item) {}
|
||||
int Num() const { return 0; }
|
||||
ElementType* GetData() { return nullptr; }
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
// Minimal TMap stand-in: just enough surface for the fixture sources to parse.
|
||||
#pragma once
|
||||
|
||||
template <typename KeyType, typename ValueType>
|
||||
class TMap
|
||||
{
|
||||
public:
|
||||
ValueType& Add(const KeyType& Key, const ValueType& Value) { static ValueType V; return V; }
|
||||
ValueType* Find(const KeyType& Key) { return nullptr; }
|
||||
bool Contains(const KeyType& Key) const { return false; }
|
||||
int Num() const { return 0; }
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
// Minimal TSet stand-in: just enough surface for the fixture sources to parse.
|
||||
#pragma once
|
||||
|
||||
template <typename ElementType>
|
||||
class TSet
|
||||
{
|
||||
public:
|
||||
void Add(const ElementType& Item) {}
|
||||
bool Contains(const ElementType& Item) const { return false; }
|
||||
int Num() const { return 0; }
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
// Stand-in for Unreal's umbrella header.
|
||||
#pragma once
|
||||
|
||||
#include "CoreTypes.h"
|
||||
#include "Containers/Array.h"
|
||||
#include "Containers/Map.h"
|
||||
#include "Containers/Set.h"
|
||||
#include "Delegates/DelegateCombinations.h"
|
||||
#include "Logging/LogMacros.h"
|
||||
#include "Math/MathFwd.h"
|
||||
#include "Misc/Optional.h"
|
||||
#include "Templates/SharedPointer.h"
|
||||
#include "Templates/SubclassOf.h"
|
||||
#include "Templates/UniquePtr.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "UObject/ObjectMacros.h"
|
||||
#include "UObject/ObjectPtr.h"
|
||||
#include "UObject/SoftObjectPtr.h"
|
||||
#include "UObject/UObjectGlobals.h"
|
||||
#include "UObject/WeakObjectPtr.h"
|
||||
@@ -0,0 +1,41 @@
|
||||
// Minimal stand-ins for core Unreal types and ubiquitous utility macros.
|
||||
// Real homes in UE: TEXT/FORCEINLINE in Misc/, assertions in Misc/AssertionMacros.h,
|
||||
// UE_DEPRECATED in Misc/CoreMiscDefines.h; folded together here for stub brevity.
|
||||
#pragma once
|
||||
|
||||
class FName
|
||||
{
|
||||
public:
|
||||
FName() = default;
|
||||
FName(const char* InName) {}
|
||||
bool operator==(const FName& Other) const { return true; }
|
||||
bool operator<(const FName& Other) const { return false; }
|
||||
};
|
||||
|
||||
class FString
|
||||
{
|
||||
public:
|
||||
FString() = default;
|
||||
FString(const char* InStr) {}
|
||||
};
|
||||
|
||||
class FText
|
||||
{
|
||||
public:
|
||||
FText() = default;
|
||||
static FText FromString(const FString& InString) { return FText(); }
|
||||
};
|
||||
|
||||
using int32 = int;
|
||||
using uint32 = unsigned int;
|
||||
using uint8 = unsigned char;
|
||||
|
||||
#define TEXT(x) x
|
||||
#define FORCEINLINE inline
|
||||
#define UE_DEPRECATED(Version, Message)
|
||||
|
||||
#define check(expr)
|
||||
#define checkf(expr, format, ...)
|
||||
#define verify(expr)
|
||||
#define ensure(expr) (!!(expr))
|
||||
#define ensureMsgf(expr, format, ...) (!!(expr))
|
||||
@@ -0,0 +1,81 @@
|
||||
// Stub of UE's delegate declaration macros (Delegates/DelegateCombinations.h).
|
||||
// Unlike the empty annotation macros, these manufacture a class at the expansion
|
||||
// site. The stubs preserve exactly that property for every delegate family:
|
||||
// single-cast, multicast, events, and the dynamic (reflection-visible) variants.
|
||||
#pragma once
|
||||
|
||||
#define DECLARE_DELEGATE(DelegateName) \
|
||||
class DelegateName \
|
||||
{ \
|
||||
public: \
|
||||
void Execute() {} \
|
||||
bool IsBound() const { return false; } \
|
||||
};
|
||||
|
||||
#define DECLARE_DELEGATE_OneParam(DelegateName, Param1Type) \
|
||||
class DelegateName \
|
||||
{ \
|
||||
public: \
|
||||
void Execute(Param1Type) {} \
|
||||
bool IsBound() const { return false; } \
|
||||
};
|
||||
|
||||
#define DECLARE_DELEGATE_RetVal(ReturnType, DelegateName) \
|
||||
class DelegateName \
|
||||
{ \
|
||||
public: \
|
||||
ReturnType Execute() { return ReturnType(); } \
|
||||
bool IsBound() const { return false; } \
|
||||
};
|
||||
|
||||
#define DECLARE_MULTICAST_DELEGATE(DelegateName) \
|
||||
class DelegateName \
|
||||
{ \
|
||||
public: \
|
||||
void Broadcast() {} \
|
||||
};
|
||||
|
||||
#define DECLARE_MULTICAST_DELEGATE_OneParam(DelegateName, Param1Type) \
|
||||
class DelegateName \
|
||||
{ \
|
||||
public: \
|
||||
void Broadcast(Param1Type) {} \
|
||||
};
|
||||
|
||||
#define DECLARE_EVENT(OwningType, EventName) \
|
||||
class EventName \
|
||||
{ \
|
||||
public: \
|
||||
void Broadcast() {} \
|
||||
};
|
||||
|
||||
#define DECLARE_DYNAMIC_DELEGATE(DelegateName) \
|
||||
class DelegateName \
|
||||
{ \
|
||||
public: \
|
||||
void ExecuteIfBound() {} \
|
||||
};
|
||||
|
||||
#define DECLARE_DYNAMIC_MULTICAST_DELEGATE(DelegateName) \
|
||||
class DelegateName \
|
||||
{ \
|
||||
public: \
|
||||
void Broadcast() {} \
|
||||
void AddDynamic(void* Object, void* Func) {} \
|
||||
};
|
||||
|
||||
#define DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(DelegateName, Param1Type, Param1Name) \
|
||||
class DelegateName \
|
||||
{ \
|
||||
public: \
|
||||
void Broadcast(Param1Type Param1Name) {} \
|
||||
void AddDynamic(void* Object, void* Func) {} \
|
||||
};
|
||||
|
||||
#define DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(DelegateName, Param1Type, Param1Name, Param2Type, Param2Name) \
|
||||
class DelegateName \
|
||||
{ \
|
||||
public: \
|
||||
void Broadcast(Param1Type Param1Name, Param2Type Param2Name) {} \
|
||||
void AddDynamic(void* Object, void* Func) {} \
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "UObject/Object.h"
|
||||
|
||||
class UWorld : public UObject
|
||||
{
|
||||
public:
|
||||
float GetTimeSeconds() const { return 0.0f; }
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "UObject/Object.h"
|
||||
|
||||
class UWorld;
|
||||
|
||||
class AActor : public UObject
|
||||
{
|
||||
public:
|
||||
virtual void BeginPlay() {}
|
||||
virtual void Tick(float DeltaSeconds) {}
|
||||
UWorld* GetWorld() const { return nullptr; }
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "GameFramework/Pawn.h"
|
||||
|
||||
class ACharacter : public APawn
|
||||
{
|
||||
public:
|
||||
virtual void Jump() {}
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "GameFramework/Actor.h"
|
||||
|
||||
class APawn : public AActor
|
||||
{
|
||||
public:
|
||||
virtual void PossessedBy(AActor* NewController) {}
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
// Stub of UE's log category macros (Logging/LogMacros.h). The DECLARE/DEFINE pair
|
||||
// manufactures a category object symbol; UE_LOG itself expands to nothing relevant.
|
||||
#pragma once
|
||||
|
||||
#define DECLARE_LOG_CATEGORY_EXTERN(CategoryName, DefaultVerbosity, CompileTimeVerbosity) \
|
||||
struct FLogCategory##CategoryName \
|
||||
{ \
|
||||
}; \
|
||||
extern FLogCategory##CategoryName CategoryName;
|
||||
|
||||
#define DEFINE_LOG_CATEGORY(CategoryName) FLogCategory##CategoryName CategoryName;
|
||||
|
||||
#define UE_LOG(CategoryName, Verbosity, Format, ...)
|
||||
@@ -0,0 +1,25 @@
|
||||
// Minimal stand-ins for UE math types.
|
||||
#pragma once
|
||||
|
||||
struct FVector
|
||||
{
|
||||
double X = 0.0;
|
||||
double Y = 0.0;
|
||||
double Z = 0.0;
|
||||
|
||||
FVector() = default;
|
||||
FVector(double InX, double InY, double InZ) : X(InX), Y(InY), Z(InZ) {}
|
||||
};
|
||||
|
||||
struct FRotator
|
||||
{
|
||||
double Pitch = 0.0;
|
||||
double Yaw = 0.0;
|
||||
double Roll = 0.0;
|
||||
};
|
||||
|
||||
struct FTransform
|
||||
{
|
||||
FVector GetLocation() const { return FVector(); }
|
||||
FRotator Rotator() const { return FRotator(); }
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
// Minimal TOptional stand-in.
|
||||
#pragma once
|
||||
|
||||
template <typename T>
|
||||
class TOptional
|
||||
{
|
||||
public:
|
||||
TOptional() = default;
|
||||
TOptional(const T& InValue) {}
|
||||
bool IsSet() const { return false; }
|
||||
const T& GetValue() const { static T Value; return Value; }
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
// Stub of UE's module boilerplate macros (Modules/ModuleManager.h).
|
||||
#pragma once
|
||||
|
||||
class FDefaultGameModuleImpl
|
||||
{
|
||||
};
|
||||
|
||||
#define IMPLEMENT_PRIMARY_GAME_MODULE(ModuleImplClass, ModuleName, GameName)
|
||||
#define IMPLEMENT_MODULE(ModuleImplClass, ModuleName)
|
||||
@@ -0,0 +1,31 @@
|
||||
// Minimal stand-ins for UE's non-UObject smart pointers.
|
||||
#pragma once
|
||||
|
||||
template <typename T>
|
||||
class TSharedPtr
|
||||
{
|
||||
public:
|
||||
TSharedPtr() = default;
|
||||
T* Get() const { return nullptr; }
|
||||
bool IsValid() const { return false; }
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class TSharedRef
|
||||
{
|
||||
public:
|
||||
T& Get() const { static T Instance; return Instance; }
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class TWeakPtr
|
||||
{
|
||||
public:
|
||||
TSharedPtr<T> Pin() const { return TSharedPtr<T>(); }
|
||||
};
|
||||
|
||||
template <typename T, typename... ArgTypes>
|
||||
TSharedRef<T> MakeShared(ArgTypes&&... Args)
|
||||
{
|
||||
return TSharedRef<T>();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Minimal TSubclassOf stand-in.
|
||||
#pragma once
|
||||
|
||||
template <typename T>
|
||||
class TSubclassOf
|
||||
{
|
||||
public:
|
||||
TSubclassOf() = default;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
// Minimal TUniquePtr stand-in.
|
||||
#pragma once
|
||||
|
||||
template <typename T>
|
||||
class TUniquePtr
|
||||
{
|
||||
public:
|
||||
TUniquePtr() = default;
|
||||
T* Get() const { return nullptr; }
|
||||
bool IsValid() const { return false; }
|
||||
};
|
||||
|
||||
template <typename T, typename... ArgTypes>
|
||||
TUniquePtr<T> MakeUnique(ArgTypes&&... Args)
|
||||
{
|
||||
return TUniquePtr<T>();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "UObject/Object.h"
|
||||
|
||||
// Base class for the U-side of UE's interface pattern (UINTERFACE).
|
||||
class UInterface : public UObject
|
||||
{
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreTypes.h"
|
||||
|
||||
class UObject
|
||||
{
|
||||
public:
|
||||
virtual ~UObject() = default;
|
||||
|
||||
template <typename T>
|
||||
T* CreateDefaultSubobject(const FName& SubobjectName)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
// Stub mirroring Unreal Engine 5.7 ObjectMacros.h (Engine/Source/Runtime/CoreUObject/
|
||||
// Public/UObject/ObjectMacros.h, lines 744-776). The annotation macros below are
|
||||
// faithful: in real UE compilation they always expand to nothing (only UnrealHeaderTool
|
||||
// parses their arguments). GENERATED_BODY is simplified: the real macro pastes
|
||||
// declarations from the per-class *.generated.h via __LINE__ token concatenation.
|
||||
#pragma once
|
||||
|
||||
#define UPROPERTY(...)
|
||||
#define UFUNCTION(...)
|
||||
#define UPARAM(...)
|
||||
#define USTRUCT(...)
|
||||
#define UMETA(...)
|
||||
#define UENUM(...)
|
||||
#define UCLASS(...)
|
||||
#define UINTERFACE(...)
|
||||
#define UDELEGATE(...)
|
||||
|
||||
#define GENERATED_BODY(...) public:
|
||||
#define GENERATED_USTRUCT_BODY(...) public:
|
||||
#define GENERATED_UCLASS_BODY(...) public:
|
||||
@@ -0,0 +1,13 @@
|
||||
// Minimal TObjectPtr stand-in (UE5's idiomatic UPROPERTY pointer wrapper).
|
||||
#pragma once
|
||||
|
||||
template <typename T>
|
||||
class TObjectPtr
|
||||
{
|
||||
public:
|
||||
TObjectPtr() = default;
|
||||
TObjectPtr(T* InPtr) {}
|
||||
T* Get() const { return nullptr; }
|
||||
T* operator->() const { return nullptr; }
|
||||
explicit operator bool() const { return false; }
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
// Minimal stand-ins for UE's soft (lazy-loadable) object references.
|
||||
#pragma once
|
||||
|
||||
template <typename T>
|
||||
class TSoftObjectPtr
|
||||
{
|
||||
public:
|
||||
TSoftObjectPtr() = default;
|
||||
T* LoadSynchronous() const { return nullptr; }
|
||||
bool IsValid() const { return false; }
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class TSoftClassPtr
|
||||
{
|
||||
public:
|
||||
TSoftClassPtr() = default;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
// Minimal stand-ins for UObject global helpers (Cast, NewObject).
|
||||
#pragma once
|
||||
|
||||
class UObject;
|
||||
|
||||
template <typename T>
|
||||
T* Cast(UObject* Object)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T* NewObject(UObject* Outer = nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Minimal TWeakObjectPtr stand-in.
|
||||
#pragma once
|
||||
|
||||
template <typename T>
|
||||
class TWeakObjectPtr
|
||||
{
|
||||
public:
|
||||
TWeakObjectPtr() = default;
|
||||
TWeakObjectPtr(T* InPtr) {}
|
||||
T* Get() const { return nullptr; }
|
||||
bool IsValid() const { return false; }
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"directory": ".",
|
||||
"command": "clang++ -std=c++20 -DTESTGAME_API= -I Stubs -I Source/TestGame -I Intermediate/TestGame/UHT -c Source/TestGame/AbilityComponent.cpp",
|
||||
"file": "Source/TestGame/AbilityComponent.cpp"
|
||||
},
|
||||
{
|
||||
"directory": ".",
|
||||
"command": "clang++ -std=c++20 -DTESTGAME_API= -I Stubs -I Source/TestGame -I Intermediate/TestGame/UHT -c Source/TestGame/AbilityActor.cpp",
|
||||
"file": "Source/TestGame/AbilityActor.cpp"
|
||||
},
|
||||
{
|
||||
"directory": ".",
|
||||
"command": "clang++ -std=c++20 -DTESTGAME_API= -I Stubs -I Source/TestGame -I Intermediate/TestGame/UHT -c Source/TestGame/GameCharacter.cpp",
|
||||
"file": "Source/TestGame/GameCharacter.cpp"
|
||||
},
|
||||
{
|
||||
"directory": ".",
|
||||
"command": "clang++ -std=c++20 -DTESTGAME_API= -I Stubs -I Source/TestGame -I Intermediate/TestGame/UHT -c Source/TestGame/TestGameModule.cpp",
|
||||
"file": "Source/TestGame/TestGameModule.cpp"
|
||||
}
|
||||
]
|
||||
@@ -23,6 +23,40 @@ def is_diagnostics_test_file(relative_path: str) -> bool:
|
||||
return filename.startswith(("diagnosticssample.", "diagnostics_sample."))
|
||||
|
||||
|
||||
def document_symbol_names(language_server: SolidLanguageServer, relative_path: str) -> list[str]:
|
||||
"""All symbol names in a file's document-symbol tree, including children."""
|
||||
symbols = language_server.request_document_symbols(relative_path).get_all_symbols_and_roots()
|
||||
symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
|
||||
names: list[str] = []
|
||||
|
||||
def _collect(syms) -> None:
|
||||
for sym in syms:
|
||||
names.append(sym.get("name"))
|
||||
_collect(sym.get("children", []) or [])
|
||||
|
||||
_collect(symbol_list)
|
||||
return names
|
||||
|
||||
|
||||
def find_document_symbol(language_server: SolidLanguageServer, relative_path: str, name: str) -> UnifiedSymbolInformation:
|
||||
"""The first symbol called ``name`` in a file's document-symbol tree; fails the test if absent."""
|
||||
symbols = language_server.request_document_symbols(relative_path).get_all_symbols_and_roots()
|
||||
symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
|
||||
|
||||
def _search(syms):
|
||||
for sym in syms:
|
||||
if sym.get("name") == name:
|
||||
return sym
|
||||
found = _search(sym.get("children", []) or [])
|
||||
if found is not None:
|
||||
return found
|
||||
return None
|
||||
|
||||
result = _search(symbol_list)
|
||||
assert result is not None, f"Symbol '{name}' not found in {relative_path}"
|
||||
return result
|
||||
|
||||
|
||||
def has_malformed_name(
|
||||
symbol: UnifiedSymbolInformation,
|
||||
whitespace_allowed: bool = False,
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
Unreal Engine 5 fixture tests for the clangd language server.
|
||||
|
||||
UE game code is written against a macro-based reflection layer (UCLASS, UFUNCTION,
|
||||
UPROPERTY, GENERATED_BODY) and engine container types (TArray, TMap). These tests
|
||||
verify that the clangd backend resolves symbols, references, definitions, and
|
||||
rename edits in the hand-written sources under Source/, and never in the
|
||||
UnrealHeaderTool-style generated files under Intermediate/ (which contain the
|
||||
same identifiers, as real generated reflection code does).
|
||||
|
||||
The fixture's stub engine headers mirror UE 5.7's ObjectMacros.h: annotation
|
||||
macros are empty in real UE compilation too (only UnrealHeaderTool parses them).
|
||||
|
||||
The fixture lives in its own repository directory (ue_test_repo) served by
|
||||
clangd only: ccls 0.20240202, the build shipped by Ubuntu and Homebrew, crashes
|
||||
intermittently when its session covers the stub engine headers, and clangd is
|
||||
the supported backend for Unreal Engine projects (see the setup guide).
|
||||
"""
|
||||
|
||||
import os
|
||||
from collections.abc import Iterator
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_utils import SymbolUtils
|
||||
from test.conftest import find_identifier_position, get_repo_path, start_ls_context
|
||||
from test.solidlsp.conftest import document_symbol_names, find_document_symbol
|
||||
|
||||
UE_REPO_PATH = get_repo_path(Language.CPP).parent / "ue_test_repo"
|
||||
ABILITY_COMPONENT_H = os.path.join("Source", "TestGame", "AbilityComponent.h")
|
||||
ABILITY_ACTOR_H = os.path.join("Source", "TestGame", "AbilityActor.h")
|
||||
ABILITY_ACTOR_CPP = os.path.join("Source", "TestGame", "AbilityActor.cpp")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def language_server() -> Iterator[SolidLanguageServer]:
|
||||
"""Clangd over ue_test_repo; overrides the shared fixture for this module."""
|
||||
with start_ls_context(Language.CPP, repo_path=str(UE_REPO_PATH)) as ls:
|
||||
yield ls
|
||||
|
||||
|
||||
@pytest.mark.cpp
|
||||
class TestClangdUnrealEngine:
|
||||
"""clangd on Unreal Engine-shaped C++ (reflection macros, engine containers)."""
|
||||
|
||||
def test_symbol_tree_contains_reflected_types(self, language_server: SolidLanguageServer) -> None:
|
||||
"""UCLASS/USTRUCT-decorated types appear in the full symbol tree."""
|
||||
symbols = language_server.request_full_symbol_tree()
|
||||
for name in ("UAbilityComponent", "AAbilityActor", "FAbilityInfo"):
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, name), f"'{name}' not found in symbol tree"
|
||||
|
||||
def test_document_symbols_show_uclass_members(self, language_server: SolidLanguageServer) -> None:
|
||||
"""UFUNCTION methods and UPROPERTY fields are visible despite the macro layer."""
|
||||
names = document_symbol_names(language_server, ABILITY_COMPONENT_H)
|
||||
for expected in ("UAbilityComponent", "TriggerAbility", "GetRemainingCooldown", "Abilities", "ActiveCooldowns"):
|
||||
assert expected in names, f"Expected '{expected}' in document symbols of AbilityComponent.h, got: {names}"
|
||||
|
||||
def test_references_resolve_to_handwritten_sources(self, language_server: SolidLanguageServer) -> None:
|
||||
"""References to a UFUNCTION are found across files, and only in Source/, never Intermediate/."""
|
||||
# Precondition: the identifier appears verbatim in generated files on disk.
|
||||
# A text search would return them; a symbol-level tool must not.
|
||||
gen_cpp = UE_REPO_PATH / "Intermediate" / "TestGame" / "UHT" / "AbilityComponent.gen.cpp"
|
||||
assert "TriggerAbility" in gen_cpp.read_text(encoding="utf-8"), "fixture broken: honeypot lost its bait"
|
||||
|
||||
trigger = find_document_symbol(language_server, ABILITY_COMPONENT_H, "TriggerAbility")
|
||||
sel_start = trigger["selectionRange"]["start"]
|
||||
refs = language_server.request_references(ABILITY_COMPONENT_H, sel_start["line"], sel_start["character"])
|
||||
ref_files = [ref.get("relativePath", "") for ref in refs]
|
||||
assert any("AbilityActor.cpp" in ref_file for ref_file in ref_files), (
|
||||
f"Expected cross-file reference in AbilityActor.cpp, got: {ref_files}"
|
||||
)
|
||||
leaked = [ref_file for ref_file in ref_files if "Intermediate" in ref_file]
|
||||
assert not leaked, f"References leaked into generated files: {leaked}"
|
||||
|
||||
def test_definition_resolves_to_source_not_generated(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Go-to-definition on a UCLASS usage lands in the hand-written header."""
|
||||
header_path = UE_REPO_PATH / "Source" / "TestGame" / "AbilityActor.h"
|
||||
position = find_identifier_position(header_path, "UAbilityComponent")
|
||||
assert position is not None, "UAbilityComponent is not used in AbilityActor.h"
|
||||
definitions = language_server.request_definition(ABILITY_ACTOR_H, position[0], position[1])
|
||||
def_paths = [d.get("relativePath", "") for d in definitions]
|
||||
assert any("AbilityComponent.h" in p for p in def_paths), f"Expected definition in AbilityComponent.h, got: {def_paths}"
|
||||
assert all("Intermediate" not in p for p in def_paths), f"Definition resolved into generated files: {def_paths}"
|
||||
|
||||
def test_symbol_locations_are_in_handwritten_sources(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Each reflected symbol's defining declaration is locatable in Source/.
|
||||
Serena's symbol edits (replace_symbol_body, insert_after_symbol) operate at
|
||||
these locations, so this is the edit-targeting guarantee. Generated headers
|
||||
may legitimately surface same-named forward declarations (real UHT output
|
||||
contains them); what matters is that the Source/ declaration is present.
|
||||
"""
|
||||
expected_files = {
|
||||
"UAbilityComponent": "AbilityComponent.h",
|
||||
"AAbilityActor": "AbilityActor.h",
|
||||
"FAbilityInfo": "AbilityTypes.h",
|
||||
"TriggerAbility": "AbilityComponent.h",
|
||||
"OnAbilityInput": "AbilityActor.h",
|
||||
}
|
||||
symbols = language_server.request_full_symbol_tree()
|
||||
found: dict[str, list[str]] = {name: [] for name in expected_files}
|
||||
|
||||
def _walk(syms):
|
||||
for sym in syms:
|
||||
name = sym.get("name")
|
||||
if name in found:
|
||||
location = sym.get("location") or {}
|
||||
found[name].append(location.get("relativePath", "") or str(location.get("uri", "")))
|
||||
_walk(sym.get("children", []) or [])
|
||||
|
||||
_walk(symbols)
|
||||
for name, expected_file in expected_files.items():
|
||||
source_locations = [p for p in found[name] if "Source" in p and expected_file in p]
|
||||
assert source_locations, f"'{name}' has no location in Source/{{...}}/{expected_file}, got: {found[name]}"
|
||||
|
||||
def test_uenum_and_enumerators_visible(self, language_server: SolidLanguageServer) -> None:
|
||||
"""UENUM-decorated enum and its UMETA-annotated enumerators appear as symbols."""
|
||||
ability_types_h = os.path.join("Source", "TestGame", "AbilityTypes.h")
|
||||
names = document_symbol_names(language_server, ability_types_h)
|
||||
for expected in ("EAbilityState", "Idle", "Active", "Cooldown"):
|
||||
assert expected in names, f"Expected '{expected}' in document symbols of AbilityTypes.h, got: {names}"
|
||||
|
||||
def test_macro_generated_delegate_type_resolves_to_source(self, language_server: SolidLanguageServer) -> None:
|
||||
"""DECLARE_DYNAMIC_MULTICAST_DELEGATE manufactures a class, which must appear
|
||||
as a symbol located at the macro expansion site in the hand-written header.
|
||||
"""
|
||||
delegate = find_document_symbol(language_server, ABILITY_COMPONENT_H, "FOnAbilityTriggered")
|
||||
location_path = (delegate.get("location") or {}).get("relativePath", "")
|
||||
assert location_path, f"FOnAbilityTriggered has no location: {delegate}"
|
||||
assert "Intermediate" not in location_path, f"Delegate symbol located in generated files: {location_path}"
|
||||
member_names = document_symbol_names(language_server, ABILITY_COMPONENT_H)
|
||||
assert "OnAbilityTriggered" in member_names, "BlueprintAssignable delegate property not visible"
|
||||
|
||||
def test_interface_pattern_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
"""The UINTERFACE pattern (paired UDamageable/IDamageable classes) yields both
|
||||
classes and the interface method; the implementing class shows the override.
|
||||
"""
|
||||
damageable_h = os.path.join("Source", "TestGame", "Damageable.h")
|
||||
names = document_symbol_names(language_server, damageable_h)
|
||||
for expected in ("UDamageable", "IDamageable", "ReceiveDamage"):
|
||||
assert expected in names, f"Expected '{expected}' in document symbols of Damageable.h, got: {names}"
|
||||
|
||||
character_h = os.path.join("Source", "TestGame", "GameCharacter.h")
|
||||
character_names = document_symbol_names(language_server, character_h)
|
||||
assert "ReceiveDamage" in character_names, "Interface override not visible in implementing class"
|
||||
|
||||
def test_log_category_symbol_in_source(self, language_server: SolidLanguageServer) -> None:
|
||||
"""DECLARE_LOG_CATEGORY_EXTERN manufactures a category object symbol in hand-written code."""
|
||||
log_h = os.path.join("Source", "TestGame", "TestGameLog.h")
|
||||
names = document_symbol_names(language_server, log_h)
|
||||
assert "LogTestGame" in names, f"Expected log category symbol 'LogTestGame', got: {names}"
|
||||
|
||||
def test_nondynamic_delegate_type_in_source(self, language_server: SolidLanguageServer) -> None:
|
||||
"""DECLARE_MULTICAST_DELEGATE (non-dynamic family) also manufactures a type in Source/."""
|
||||
delegate = find_document_symbol(language_server, ABILITY_COMPONENT_H, "FOnCooldownExpired")
|
||||
location_path = (delegate.get("location") or {}).get("relativePath", "")
|
||||
assert location_path, f"FOnCooldownExpired has no location: {delegate}"
|
||||
assert "Intermediate" not in location_path, f"Delegate symbol located in generated files: {location_path}"
|
||||
|
||||
def test_character_members_with_ue_types_visible(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Members typed with FVector/TObjectPtr/TWeakObjectPtr/TSoftObjectPtr and a
|
||||
UPARAM(ref) UFUNCTION are all visible in the symbol tree.
|
||||
"""
|
||||
character_h = os.path.join("Source", "TestGame", "GameCharacter.h")
|
||||
names = document_symbol_names(language_server, character_h)
|
||||
for expected in ("AGameCharacter", "SpawnOffset", "Abilities", "CurrentTarget", "FallbackLoadout", "Heal"):
|
||||
assert expected in names, f"Expected '{expected}' in document symbols of GameCharacter.h, got: {names}"
|
||||
|
||||
def test_rename_edit_targets_only_source_files(self, language_server: SolidLanguageServer) -> None:
|
||||
"""A rename WorkspaceEdit for a UFUNCTION touches only hand-written files (edit is not applied)."""
|
||||
trigger = find_document_symbol(language_server, ABILITY_COMPONENT_H, "TriggerAbility")
|
||||
sel_start = trigger["selectionRange"]["start"]
|
||||
edit = language_server.request_rename_symbol_edit(ABILITY_COMPONENT_H, sel_start["line"], sel_start["character"], "ActivateAbility")
|
||||
assert edit is not None, "clangd should support rename"
|
||||
|
||||
touched: list[str] = []
|
||||
for uri in edit.get("changes") or {}:
|
||||
touched.append(uri)
|
||||
for doc_change in edit.get("documentChanges") or []:
|
||||
text_doc = doc_change.get("textDocument") or {}
|
||||
if text_doc.get("uri"):
|
||||
touched.append(text_doc["uri"])
|
||||
|
||||
assert any("AbilityComponent.h" in uri for uri in touched), f"Rename does not edit the declaring header, touched: {touched}"
|
||||
leaked = [uri for uri in touched if "Intermediate" in uri]
|
||||
assert not leaked, f"Rename would edit generated files: {leaked}"
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Unreal Engine handling in the clangd and ccls servers, without starting a process.
|
||||
|
||||
No ``cpp`` marker: these build server shells with ``object.__new__`` and call the no-database
|
||||
and directory-ignore code paths directly, so they run in the default suite.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp.language_servers.ccls_language_server import CCLS
|
||||
from solidlsp.language_servers.clangd_language_server import ClangdLanguageServer
|
||||
from solidlsp.language_servers.common import UE_IGNORED_DIRNAMES, is_unreal_engine_project
|
||||
from solidlsp.ls_exceptions import SolidLSPException
|
||||
|
||||
|
||||
def _make_clangd(root) -> ClangdLanguageServer:
|
||||
server = object.__new__(ClangdLanguageServer)
|
||||
server.repository_root_path = str(root)
|
||||
return server
|
||||
|
||||
|
||||
def _make_ccls(root) -> CCLS:
|
||||
server = object.__new__(CCLS)
|
||||
server.repository_root_path = str(root)
|
||||
return server
|
||||
|
||||
|
||||
def test_is_unreal_engine_project_detection(tmp_path):
|
||||
assert is_unreal_engine_project(str(tmp_path)) is False
|
||||
(tmp_path / "MyGame.uproject").write_text("{}")
|
||||
assert is_unreal_engine_project(str(tmp_path)) is True
|
||||
|
||||
|
||||
def test_missing_compile_commands_raises_for_unreal_project(tmp_path):
|
||||
(tmp_path / "MyGame.uproject").write_text("{}")
|
||||
server = _make_clangd(tmp_path)
|
||||
|
||||
with pytest.raises(SolidLSPException, match="Unreal Engine"):
|
||||
server._prepare_compile_commands()
|
||||
|
||||
|
||||
def test_missing_compile_commands_returns_none_for_non_unreal_project(tmp_path):
|
||||
server = _make_clangd(tmp_path)
|
||||
assert server._prepare_compile_commands() is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("make_server", [_make_clangd, _make_ccls])
|
||||
def test_unreal_dirs_ignored_only_for_unreal_project(make_server, tmp_path):
|
||||
non_ue = make_server(tmp_path)
|
||||
for dirname in UE_IGNORED_DIRNAMES:
|
||||
assert not non_ue.is_ignored_dirname(dirname), f"{dirname} should not be pruned without a .uproject"
|
||||
|
||||
ue_root = tmp_path / "ue"
|
||||
ue_root.mkdir()
|
||||
(ue_root / "MyGame.uproject").write_text("{}")
|
||||
ue = make_server(ue_root)
|
||||
for dirname in UE_IGNORED_DIRNAMES:
|
||||
assert ue.is_ignored_dirname(dirname), f"{dirname} should be pruned for a UE project"
|
||||
|
||||
assert non_ue.is_ignored_dirname(".ccls-cache")
|
||||
assert ue.is_ignored_dirname(".ccls-cache")
|
||||
Reference in new issue
Block a user