Skip to content
June Rhodes edited this page Oct 1, 2025 · 8 revisions

This fork of LLVM/Clang allows you to define static analysis rules in .clang-rules files and have them automatically run during compilation. You do not need to update any other tooling, and it can be used as a drop-in replacement for normal Clang in Unreal Engine builds.

This version of Clang also understands the Unreal Engine UCLASS etc. specifiers so you can match against them. See the AST Matcher Reference page which provides a list of available matchers you can use.

Download and Install

You can download the latest version of Clang for Unreal Engine from GitHub.

Download the llvm-win64-18.x artifact, which will give you a ZIP file. Extract this ZIP file to C:\Program Files\LLVM such that C:\Program Files\LLVM\bin\clang.exe exists.

To use Clang instead of MSVC in the Unreal build system, open %appdata%\Unreal Engine\UnrealBuildTool\BuildConfiguration.xml (create it if it does not exist), and set the WindowsPlatform/Compiler setting like so:

<?xml version="1.0" encoding="utf-8"?>
<Configuration xmlns="https://www.unrealengine.com/BuildConfiguration">
  <WindowsPlatform>
    <Compiler>Clang</Compiler>
  </WindowsPlatform>
</Configuration>

Getting Started

After installing this fork of Clang, in any directory that you want to have static analysis rules run, create a .clang-rules file. The rules and rulesets you define in this file will apply to all header and source files within and under the directory that the .clang-rules file is in.

When you're creating a .clang-rules file, you'll need to pick a namespace. This should be something unique to your organisation. If you need to reference rules across namespaces in rulesets, use the form namespace/rule.

An example .clang-rules file that we use internally for EOS Online Subsystem looks as follows:

Namespace: redpoint.games
Rulesets:
  Name: eos-online-subsystem
  Severity: Error
  Rules:
  - using-namespace-in-non-file-namespace
  - using-in-file-namespace
  - incorrectly-nested-namespace
  - field-not-initialized
  - performance-unnecessary-value-param
  - make-shared-with-events
  - use-designated-init
Rules:
- # Find function parameters that could be 'const &'.
  # @note: We exclude functions starting with 'On' because we assume they might
  # have delegate captures that must not be passed by reference.
  Name: performance-unnecessary-value-param
  Matcher: |
    functionDecl(
      hasBody(stmt()), 
      isDefinition(), 
      unless(isImplicit()),
      unless(matchesName("::On.*")),
      unless(cxxMethodDecl(anyOf(isOverride(), isFinal()))),
      has(typeLoc(forEach(
        parmVarDecl(
          hasType(qualType(
            hasCanonicalType(isExpensiveToCopy()),
            unless(hasCanonicalType(referenceType())))),
          decl().bind("param")
        )
      ))),
      unless(isInstantiated()), decl().bind("functionDecl")
    )
  ErrorMessage: |
    parameter should be made 'const &' to avoid unnecessary copy
  Callsite: param
- # Find initializers that are undesignated (requires C++20)
  Name: use-designated-init
  Matcher: |
    initListExpr(
      hasType(
        cxxRecordDecl(
          isAggregate(),
          anyOf(
            hasName("::Redpoint::EOS::API::Lobby::FJoinLobby::Options"),
            hasName("::Redpoint::EOS::API::Lobby::FJoinLobbyById::Options"),
            hasName("::Redpoint::EOS::API::Lobby::FCreateLobby::Options")
          ),
          unless(hasAnyBase(hasType(cxxRecordDecl(has(fieldDecl())))))
        ).bind("type")
      ),
      unless(isFullyDesignated())
    ).bind("init")
  ErrorMessage: |
    initializer list should be fully designated
  Callsite: init
- # Find MakeShared<T> where T inherits from IHasEventRegistration
  Name: make-shared-with-events
  Matcher: |
    callExpr(
      callee(
        functionDecl(
          hasName("MakeShared"),
          hasTemplateArgument(0, refersToType(recordType(
            hasDeclaration(cxxRecordDecl(isDerivedFrom("IHasEventRegistration")))
          )))
        )
      )
    ).bind("call")
  ErrorMessage: |
    must use MakeSharedWithEvents instead of MakeShared for this type
  Callsite: call
- # Disallow 'using namespace' outside of non-file namespaces.
  Name: using-namespace-in-non-file-namespace
  Matcher: |
    namespaceDecl(
      unless(isAnonymous()),
      unless(hasAncestor(namespaceDecl(matchesName("::__File([0-9]+)_Redpoint")))),
      has(usingDirectiveDecl(unless(isImplicit())).bind("using_decl"))
    )
  ErrorMessage: |
    'using namespace' is only permitted inside 'REDPOINT_EOS_FILE_NS_ID()'
  Callsite: using_decl
- # 'Redpoint' namespace underneath 'EOS', which usually means that the exporting
  # namespace block is underneath the file-isolated namespace
  Name: incorrectly-nested-namespace
  Matcher: |
    namespaceDecl(
      hasName("Redpoint"), 
      hasAncestor(namespaceDecl(hasName("EOS")).bind("containing_decl"))
    ).bind("export_decl")
  ErrorMessage: |
    detected export namespace underneath file-isolated namespace
  Callsite: export_decl
  Hints:
    containing_decl: containing namespace found here
- # Disallow 'using =' in file namespaces, as this usually indicates an
  # error in the namespace export.
  Name: using-in-file-namespace
  Matcher: |
    namespaceDecl(
      unless(isAnonymous()),
      hasAncestor(namespaceDecl(matchesName("::__File([0-9]+)_Redpoint"))),
      has(usingDecl(unless(isImplicit())).bind("using_decl"))
    )
  ErrorMessage: |
    'using <type>' is not permitted inside 'REDPOINT_EOS_FILE_NS_ID()' (was this meant to be an export namespace?)
  Callsite: using_decl
- # Detects if a field in a class or struct is not initialized in the 
  # constructor's initialization list when at least one member is initialized
  # via the initializer list.
  Name: field-not-initialized
  Matcher: |
    cxxConstructorDecl(
      unless(isImplicit()),
      unless(isDelegatingConstructor()),
      unless(isDeleted()),
      unless(isDefaulted()),
      hasBody(stmt()),
      unless(ofClass(cxxRecordDecl(isUClass()))),
      unless(ofClass(cxxRecordDecl(isUInterface()))),
      ofClass(cxxRecordDecl(forEach(fieldDecl().bind("declared_field")))),
      forNone(cxxCtorInitializer(forField(fieldDecl(equalsBoundNode("declared_field")).bind("referenced_field"))))
    ).bind("bad_constructor")
  ErrorMessage: |
    one or more fields will be uninitialized when class or struct is constructed; please add the field to the initializer list.
  Callsite: bad_constructor
  Hints:
    declared_field: this field must be initialized
Clone this wiki locally