Menu

#497 Formatter formats "Contains" keyword which isn't necessarily a reserved keyword

Closed
closed-fixed
formatter (29)
5
2026-05-30
2026-05-29
No

How it should format:

procedure TMyClass.AddUnique(const S: string);
begin
  if not Contains(S) then
    DoAdd(S);
end;

How it currently formats:

procedure TMyClass.AddUnique(const S: string);
begin


  if contains
  (S) then
  DoAdd(S);
end;

Discussion

  • Luka Havrlišan

    Luka Havrlišan - 2026-05-30

    Here's a working fix Claude gave me after describing the issue. It also handles other keywords with the same bug.

    { GX_CodeFormatterFormatter.pas - TCodeFormatterFormatter }
    
    { interface section - new function: UsesUsedAsIdentifier }
        ///<summary>
        /// @returns True if the current rtUses token ("uses"/"exports"/"requires"/"contains")
        ///          is actually being used as an ordinary identifier rather than a section
        ///          keyword. "contains" and "requires" are context-sensitive directives that
        ///          are only keywords inside package (.dpk) files, so they are common
        ///          identifiers (e.g. a method named "Contains") in normal code. </summary>
        function UsesUsedAsIdentifier: Boolean;
    
    { implementation section - new function: UsesUsedAsIdentifier }
    function TCodeFormatterFormatter.UsesUsedAsIdentifier: Boolean;
    var
      Next: TPascalToken;
      Prev: TPascalToken;
      Idx: Integer;
    begin
      // A genuine "uses"/"exports"/"requires"/"contains" section keyword is always followed
      // by a unit identifier and only ever appears at the start of a declaration, so the
      // token preceding it is the program/unit header, "interface", "implementation" or ";".
      // It is never followed by "(" and never preceded by an operator, "(", "[" or ",".
      // "contains" and "requires" in particular are context-sensitive directives (keywords
      // only inside package .dpk files), so they are common identifiers in ordinary code.
    
      // a) Function/method call, e.g. "if not Contains(S) then" - a clause is never followed by "(".
      if TryGetNextNoComment(FTokenIdx, Next) and (Next.ReservedType = rtLeftBr) then begin
        Result := True;
        Exit; //==>
      end;
    
      // b) Used as an operand, e.g. "if not Contains then", "X := Requires", "(Contains)",
      //    "Foo(A, Contains)". Find the previous meaningful token (skip line feeds and comments).
      Prev := nil;
      Idx := FTokenIdx - 1;
      while TryGetToken(Idx, Next) do begin
        if not (Next.ReservedType in [rtLineFeed, rtComment]) then begin
          Prev := Next;
          Break; //==v
        end;
        Dec(Idx);
      end;
      Result := (Prev <> nil) and (Prev.ReservedType in [rtOper, rtMathOper, rtLogOper,
        rtMinus, rtPlus, rtEquals, rtAssignOper, rtLeftBr, rtLeftHook, rtComma]);
    end;
    
    { implementation section - update function: CheckIndent }
        rtProgram, rtUses, rtInitialization:
    --      if (FCurrentRType = rtUses) and (FStack.GetTopType in [rtProcedure, rtProcDeclare, rtClass]) then begin
    ++      if (FCurrentRType = rtUses)
    ++        and ((FStack.GetTopType in [rtProcedure, rtProcDeclare, rtClass])
    ++          or UsesUsedAsIdentifier) then begin
    ++        // The word is an ordinary identifier, not a "uses"/"exports"/"requires"/"contains"
    ++        // section keyword - treat it as a normal word. (The qualified case "obj.Contains"
    ++        // is already handled above where rtDot precedes the word.)
            FCurrentToken.SetReservedType(rtNothing);
            FCurrentRType := rtNothing;
    

    It also wrote a test file for this case (all /expected/ profiles share the same syntax, except special which has additional whitespace before not keyword in if conditional statements):

    unit testfile_ContainsAsIdentifier;
    
    interface
    
    type
      TMyClass = class
      private
        function Contains(const S: string): Boolean;
        function Requires: Boolean;
        procedure DoAdd(const S: string);
      public
        procedure AddUnique(const S: string);
        procedure QualifiedCall(const S: string);
        function BareIdentifiers: Boolean;
      end;
    
    implementation
    
    procedure TMyClass.AddUnique(const S: string);
    begin
      if not Contains(S) then
        DoAdd(S);
    end;
    
    procedure TMyClass.QualifiedCall(const S: string);
    begin
      if not Self.Contains(S) then
        DoAdd(S);
    end;
    
    function TMyClass.BareIdentifiers: Boolean;
    begin
      // contains/requires used as bare identifiers (no parens, no leading dot)
      if not Contains then
        DoAdd('x');
      Result := Requires and (not Contains);
    end;
    
    end.
    
     

    Last edit: Luka Havrlišan 2026-05-30
  • Thomas Mueller

    Thomas Mueller - 2026-05-30
    • status: open --> closed-fixed
    • assigned_to: Thomas Mueller
    • Group: New --> Closed
     
  • Thomas Mueller

    Thomas Mueller - 2026-05-30

    fixed in revision 5467 (using your code as basis, thanks)

     

Log in to post a comment.