Skip to content

Common Rules

June Rhodes edited this page Sep 25, 2024 · 7 revisions

The following ruleset and rules are provided for you to copy into your own project. These rules detect common issues when writing Unreal Engine C++ code.

General C++ Rules

# Append this after your own namespace/rules/rulesets, including the '---' separator.
---
Namespace: llvm.redpoint.games
Rulesets:
- Name: clang-tidy
  Severity: Error
  Rules:
  - performance-unnecessary-value-param
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

Unreal Engine Rules

# Append this after your own namespace/rules/rulesets, including the '---' separator.
---
Namespace: unreal.redpoint.games
Rulesets:
- Name: unreal-engine
  Severity: Error
  Rules:
  - Name: ionlinesubsystem-get
    Severity: Warning
  - Name: unsafe-storage-of-oss-pointer
    Severity: Warning
  - broken-array-call
  - bad-reference-capture-in-delegate
  - missing-uproperty
  - missing-super-call-lifetimeprops
  - missing-doreplifetime-for-replicated-property
  - incorrect-interface-invocation
  - ustruct-field-not-initialized-in-class
    # Enable these rules if you're building plugins for the Marketplace:
  - ufunction-must-have-category
  - uproperty-must-have-category
Rules:
- # Detects usages of array functions where the item parameter is a reference
  # back into the array, and where the reference originates from a for-range
  # loop.
  #
  # This detects bad code like this:
  #
  # for (const auto& Val : Arr)
  # {
  #     Arr.Remove(Val);
  # }
  #
  # which is invalid because the array will free the memory that Val is
  # pointing to. Unreal checks this at runtime and will assert; this clang-tidy
  # rule detects it at compile time.
  #
  # To fix this code, make the iteration value a copy of the value
  # from the array, or index into the array instead of using a for-range loop:
  #
  # for (auto Val : Arr)
  # {
  #     Arr.Remove(Val);
  # }
  Name: broken-array-call
  Matcher: |
    cxxMemberCallExpr(
      on(
        declRefExpr(
          hasType(cxxRecordDecl(hasName("TArray"))), 
          to(decl().bind("array_declared_here"))
        ).bind("array_callsite")
      ), 
      callee(
        cxxMethodDecl(
          matchesName("(Insert|Insert_GetRef|Add|Add_GetRef|Remove|RemoveSwap)")
        )
      ), 
      hasArgument(
        0, 
        declRefExpr(
          to(
            varDecl(
              hasType(referenceType()), 
              hasAncestor(
                cxxForRangeStmt(
                  hasRangeInit(
                    declRefExpr(
                      to(decl(equalsBoundNode("array_declared_here")))
                    )
                  )
                ).bind("array_for_range")
              )
            ).bind("dangerous_ref_declaration")
          )
        ).bind("dangerous_ref_usage")
      )
    ).bind("bad_callsite")
  ErrorMessage: |
    incorrect usage of mutating array call with non-copy will lead to crash at runtime
  Callsite: bad_callsite
  Hints:
    array_for_range: "make this a copy instead of a ref (i.e. not const&), or switch to an index-based for loop"
- # Detects when you capture a reference as a user parameter to a delegate. This is almost always incorrect, as the delegate will capture the reference, not the value. When the referenced memory goes out of scope, the delegate can be invoked with arguments that point to an invalid memory space.
  #
  # For example:
  #
  # DECLARE_DELEGATE(FSomeFunctionHandler)
  # void FunctionHandler(const int& UserParam) {};
  # void BadImpl()
  # {
  #     int A = 5;
  #     FSomeFunctionHandler::CreateStatic(&FunctionHandler, A);
  #     // If CreateStatic was passed to an event to be called after BadImpl
  #     // returns, it would be invoked with UserParam pointing to invalid memory.
  # }
  #
  # You should always pass user parameters by value.
  Name: bad-reference-capture-in-delegate
  Matcher: |
    declRefExpr(to(functionDecl(hasName("(Create|Bind)(Lambda|Raw|SP|Static|ThreadSafeSP|UFunction|UObject|WeakLambda)"), isTemplateInstantiation(), hasAnyTemplateArgument(refersToPack(refersToType(lValueReferenceType().bind("captured_reference"))))))).bind("bad_callsite")
  ErrorMessage: |
    creating or binding of delegate incorrectly captures reference
  Callsite: bad_callsite
  Hints:
    captured_reference: "this is the reference that is incorrectly captured. change the function declaration so this is passed by value instead."
- # Detects when you forget to call Super::BeginDestroy(); in an overridden BeginDestroy function.
  Name: missing-super-call-begindestroy
  Matcher: |
    cxxMethodDecl(
      isOverride(),
      hasName("BeginDestroy"),
      hasBody(compoundStmt()),
      unless(
        hasDescendant(
          cxxMemberCallExpr(
            callee(cxxMethodDecl(hasName("BeginDestroy"))),
            on(cxxThisExpr())
          )
        )
      )
    ).bind("bad_impl")
  ErrorMessage: |
    missing call to Super::BeginDestroy(). make sure you call Super::BeginDestroy() in your overridden implementation.
  Callsite: bad_impl
- # Detects when you use IOnlineSubsystem::Get(). You should always use Online::GetSubsystem() instead.
  Name: ionlinesubsystem-get
  Matcher: |
    callExpr(
      callee(
        cxxMethodDecl(
          ofClass(
            cxxRecordDecl(
              hasName("IOnlineSubsystem")
            )
          ),
          hasName("Get")
        )
      )
    ).bind("bad_callsite")
  ErrorMessage: |
    use Online::GetSubsystem(this->GetWorld()) instead of IOnlineSubsystem::Get(), as IOnlineSubsystem::Get() can't be safely used in the editor.
  Callsite: bad_callsite
- # Detects when you try to store an IOnlineSubsystemPtr or IOnline*Ptr as a field in a class or struct.
  #
  # You can not safely store these values, as they will prevent the online subsystem from releasing it's resources, which it may do when the editor is open and play-in-editor is started or stopped.
  #
  # You should use weak pointers e.g. TWeakPtr<IOnlineIdentity, ESPMode::ThreadSafe> instead.
  Name: unsafe-storage-of-oss-pointer
  Matcher: |
    fieldDecl(
      hasType(
        hasUnqualifiedDesugaredType(
          recordType(
            hasDeclaration(
              classTemplateSpecializationDecl(
                hasName("TSharedPtr"),
                hasTemplateArgument(
                  0,
                  refersToType(
                    hasDeclaration(
                      namedDecl(
                        matchesName("IOnline[A-Za-z0-9]+")
                      )
                    )
                  )
                )
              )
            )
          )
        )
      )
    ).bind("bad_field")
  ErrorMessage: |
    storing shared pointers of this type at the struct or class level is unsafe, as it will prevent the online subsystem from safely releasing it's resources. use a TWeakPtr<> instead.
  Callsite: bad_field
- # Detects when you forget to add UPROPERTY() to fields in a UCLASS(), where those fields point to other garbage collected objects.
  Name: missing-uproperty
  Matcher: |
    fieldDecl(
      unless(
        isUProperty()
      ), 
      hasParent(
        cxxRecordDecl(
          isUClass()
        )
      ), 
      hasType(
        pointerType(
          pointee(
            recordType(
              hasDeclaration(
                cxxRecordDecl(
                  isUClass()
                )
              )
            )
          )
        )
      )
    ).bind("bad_field")
  ErrorMessage: |
    missing UPROPERTY() declaration on this field, which is required for all UObject pointers
  Callsite: bad_field
- # Detects when you forget to call Super::GetLifetimeReplicatedProps(); in an overridden GetLifetimeReplicatedProps function.
  Name: missing-super-call-lifetimeprops
  Matcher: |
    cxxMethodDecl(
      isOverride(),
      hasName("GetLifetimeReplicatedProps"),
      hasBody(compoundStmt()),
      ofClass(
        cxxRecordDecl(
          isDirectlyDerivedFrom(
            typedefNameDecl(
              hasType(
                type().bind("parent_class")
              )
            )
          )
        )
      ),
      unless(
        hasDescendant(
          cxxMemberCallExpr(
            callee(
              cxxMethodDecl(
                hasName("GetLifetimeReplicatedProps")
              )
            ),
            anyOf(
              thisPointerType(
                qualType(
                  recordType(
                    equalsBoundNode("parent_class")
                  )
                )
              ),
              onImplicitObjectArgument(
                implicitCastExpr(
                  has(
                    implicitCastExpr(
                      hasImplicitDestinationType(
                        qualType(
                          pointsTo(
                            typedefType(
                              hasDeclaration(
                                typedefDecl(
                                  hasName("Super")
                                )
                              )
                            )
                          )
                        )
                      )
                    )
                  )
                )
              )
            )
          )
        )
      )
    ).bind("bad_impl")
  ErrorMessage: |
    missing call to Super::GetLifetimeReplicatedProps(). make sure you call Super::GetLifetimeReplicatedProps() in your overridden implementation.
  Callsite: bad_impl
- # Detects when you forget to call DOREPLIFETIME() for a replicated property.
  Name: missing-doreplifetime-for-replicated-property
  Matcher: |
    cxxMethodDecl(
      hasName("GetLifetimeReplicatedProps"), 
      ofClass(
        cxxRecordDecl(
          forEach(
            fieldDecl(
              isUProperty(), 
              hasUSpecifier("replicated")
            ).bind("declared_property")
          )
        )
      ), 
      hasBody(
        compoundStmt(
          forNoDescendant(
            callExpr(
              callee(
                namedDecl(
                  hasName("GetMemberNameCheckedJunk")
                )
              ), 
              hasArgument(
                0, 
                memberExpr(
                  member(
                    fieldDecl(
                      equalsBoundNode("declared_property")
                    )
                  )
                )
              )
            )
          )
        )
      )
    ).bind("configuration_site")
  ErrorMessage: |
    missing call to DOREPLIFETIME() or equivalent macro in GetLifetimeReplicatedProps for a replicated property.
  Callsite: declared_property
  Hints:
    configuration_site: "this implementation needs to call DOREPLIFETIME() or an similar macro to configure this property for replication"
- # Detects when you call an IInterface method directly with I->Test(); instead of using IInterface::Execute_Test(I);
  Name: incorrect-interface-invocation
  Matcher: |
    cxxMemberCallExpr(
      thisPointerType(
        hasDeclaration(
          cxxRecordDecl(
            isIInterface()
          )
        )
      ),
      callee(
        cxxMethodDecl(
          isUFunction(),
          anyOf(
            hasUSpecifier("BlueprintImplementableEvent"),
            hasUSpecifier("BlueprintNativeEvent")
          )
        )
      )
    ).bind("bad_callsite")
  ErrorMessage: |
    incorrect call to UInterface member method. you must call methods via IInterface::Execute_Func(Val, Arg1, Arg2, ...) instead of Val->Func(Arg1, Arg2, ...).
  Callsite: bad_callsite
- # Detects when a USTRUCT property does not have an in-class initializer. This causes a runtime warning as of Unreal Engine 5 and is marked to cause a runtime error in future, so this check catches the issue at compile time.
  Name: ustruct-field-not-initialized-in-class
  Matcher: |
    fieldDecl(
      hasType(qualType(isPODType())),
      unless(hasType(cxxRecordDecl(isUStruct()))),
      unless(hasInClassInitializer(expr())),
      hasDeclContext(cxxRecordDecl(isUStruct()))
    ).bind("bad_field")
  ErrorMessage: |
    missing default initializer for POD-typed field declaration in USTRUCT (expected a zeroing initializer like 'int A = 0;' or 'bool B = false;')
  Callsite: bad_field
- # Detects when a UFUNCTION is missing the 'Category' specifier, which is required for plugins that are submitting to the Marketplace/Fab.
  Name: ufunction-must-have-category
  Matcher: |
    namedDecl(
      isUFunction(),
      anyOf(
        hasUSpecifier("BlueprintCallable"), 
        hasUSpecifier("BlueprintPure")),
      unless(anyOf(
        hasUMetadata("Category"),
        hasUSpecifier("Category"))),
      unless(hasUSpecifier("BlueprintSetter")),
      unless(hasUSpecifier("BlueprintGetter")),
      unless(hasUSpecifier("BlueprintInternalUseOnly")),
      unless(hasUSpecifier("DeprecatedFunction"))
    ).bind("decl")
  ErrorMessage: |
    UFUNCTION is missing 'Category' specifier
  Callsite: decl
- # Detects when a UPROPERTY is missing the 'Category' specifier, which is required for plugins that are submitting to the Marketplace/Fab.
  Name: uproperty-must-have-category
  Matcher: |
    namedDecl(
      isUProperty(),
      anyOf(
        hasUSpecifier("BlueprintVisible"), 
        hasUSpecifier("EditAnywhere"),
        hasUSpecifier("EditDefaultsOnly"), 
        hasUSpecifier("EditInstanceOnly")),
      unless(anyOf(
        hasUMetadata("Category"),
        hasUSpecifier("Category")))
    ).bind("decl")
  ErrorMessage: |
    UPROPERTY is missing 'Category' specifier
  Callsite: decl