Jump to content
Mike Torrettinni

Can I force compiler to report on enum change that related code needs changing?

Recommended Posts

I'm trying to make compiler complain when adding new enum values and related code is not ready for new values:

 

here is simple example of Project types and setting project names:

 

type
  TProjectType = (ptMain, ptExternal, ptDivision, ptBranch);
  TProjectNames = array[TProjectType] of string;

var
  xProjectNames: TProjectNames;

  procedure SaveProjectNames(aProjNames: TProjectNames);
  begin
    xProjectNames := aProjNames;
  end;

  function Convert(aProjNames: array of string): TProjectNames;
  var i: integer;
  begin
    Assert(Length(aProjNames) = (Ord(High(TProjectType)) + 1));

    for i := Low(aProjNames) to High(aProjNames) do
      Result[TProjectType(i)] := aProjNames[i];
  end;

begin
  //SaveProjectNames(['a','b','c','d']); <- This doesn't work, so I need use Convert function to convert from open array to correct type
  SaveProjectNames(Convert(['a','b','c','d']));
end.

 

When I add new project type into TProjectType, like ptCountry,  I want compiler to complain that any related code, like SaveProjectNames should pass more values.

With Assert in Convert function I make sure that everything starts failing in runtime, but it would be nice to have compiler show all the code that needs fixing.

 

It would be easy if SaveProjectNames(['a','b','c','d']); would work, and then compiler would say it's not enough parameters, but open arrays and non-open arrays don't mix.

 

Is that possible in my case?

Share this post


Link to post
16 minutes ago, Mike Torrettinni said:

I'm trying to make compiler complain when adding new enum values and related code is not ready for new values:

 

here is simple example of Project types and setting project names:

 


type
  TProjectType = (ptMain, ptExternal, ptDivision, ptBranch);
  TProjectNames = array[TProjectType] of string;

const
  CProjectNames : Array[ TProjectType ] of String = ( 'A', 'B', 'C', 'D'); //<== If it can be fixes constant array, which crashes when non-matching enums

 

Not sure if I got your point right, you want a message/crash if a new type is added on the enum, to get noted, right ?

  • Thanks 1

Share this post


Link to post
15 minutes ago, Rollo62 said:

Not sure if I got your point right, you want a message/crash if a new type is added on the enum, to get noted, right ?

Correct, compiler message that complains.. not sure which message that would be, but any alert that would show me all the code that is using this enum in a way that is not compatible anymore with new size of enum.

The const is not good, values are dynamic.

Share this post


Link to post
1 hour ago, Mike Torrettinni said:

Correct, compiler message that complains.. not sure which message that would be, but any alert that would show me all the code that is using this enum in a way that is not compatible anymore with new size of enum.

The const is not good, values are dynamic.

I have need of values that can be persisted to a DB, so am now using RTTI to let me save the enum members by name, not their ordinal values. That frees me from any dependence on order or number. I don't know of any way to directly achieve what you want. 

Share this post


Link to post

I was hoping there is a simple solution. With most of enums this is not a problem, but I have a few that span in multiple UI controls and that is something can't be simply changed.

For example: checkboxes to select filter on project type - when new project type is added in enum, I need to add new checkbox on the form. If I can have compiler complain at the level where these checkboxes are transformed into data filter, I can see that a value/parameter is missing -> add new checkbox.

Share this post


Link to post
Guest

I don't think the compiler is much of a help here, but i can list two things from my point of view

 

1) In case you are using case switches and listing values, then as rule of thumb, always or almost always (this is up to you) use default value to return "Uknown" or raise an exception, specially with enums that might evolve over time, this also will work when you are using if's, just remember to make your code future proof.

 

2) Connecting ComboBoxes and Enums means you might be using RTTI and resolving the names or something, not sure how to do it as never needed it, also may be there is more than one path to take using RTTI, what you need is to resolve the count of that Enum and use initialization section in its unit to call an assert (or simply raise an exception), not like a compiler error, but the next best thing, your application will not run.

Share this post


Link to post
Guest

Well thinking about it, it is doable without using RTTI

type
  TMyEnum = (meOne, meTwo, meNine, meTen);

procedure CheckTMyEnumValues;
var
  I: TMyEnum;
begin
  for I := Low(TMyEnum) to High(TMyEnum) do
    if not (I in [meOne, meTwo, meTen]) then
      raise Exception.Create('TMyEnum has been changed');
end;

begin      // initialization
  CheckTMyEnumValues;

Your target is to break more things when things already and silently broken, so the above will do that, after any to TMyEnum, you will need to copy and paste all values in that if range.

Share this post


Link to post

It needs some code to be added, but you can declare a const array[<enumeration>] of Integer with values 0 in all places where the enumeration is used in some way.

const
  cCheckProjectType: array[TProjectType] of Integer = (0, 0, 0, 0); 

If you now extend TProjectType the compiler will stop at all places where such a declaration is present and you can inspect the code around for correct TProjectType usage. After that is done you extend the array values to make the compiler continue to the next problematic position.

  • Like 4

Share this post


Link to post

Thanks, interesting suggestions, but they don't fit the need. I think this is my problem of how I designed reports. In cases where selection is a Listbox, the new project type will be automatically added as additional item because they get generated from enum.

But in cases where I manually put checkbox on the form for each project type, I will just need to find them and add new one, when needed.

 

I guess best option is to have 1 common method that creates a filter from selection of project types and then I can follow where this method is used, when new project is added.

 

It would be really nice if compiler could distinguish Set, open arrays and enum arrays, so we can pass ['a','b','c'] into array[enum] of string. And then if enum changes to have 2 or 4 elements it would complain. Seems like a small update to compiler 🙂

 

 

 

Share this post


Link to post

Still not getting your point.

If the texts were dynamic, you should have to check them too anyway, not only the enum itself. 

 

Share this post


Link to post
Quote

It would be really nice if compiler could distinguish Set, open arrays and enum arrays, so we can pass ['a','b','c'] into array[enum] of string. And then if enum changes to have 2 or 4 elements it would complain. Seems like a small update to compiler

Try overloading to extend your codebase.  Or explore it in this case
 

//existing
  procedure SaveProjectNames(aProjNames: TProjectNames); overload;
  begin
    xProjectNames := aProjNames;
  end;

  
  //add
  procedure SaveProjectNames(aitems: TStrings); overload;
  begin
    ...  
    for switches in Atems 
	SaveProjectNames(switches[0],switches[1], False);
  end;
  
  
  //    this overload wants three args need to set the extra args to False in existing 
  procedure SaveProjectNames(abMain, abXXX, abCountry: Strings); overload;
  begin
    If abMain = 'C' do stuff
	If ...
	//If abCountry  
  end;

force the compiler to use new procedures with fixed number of Args
ie comment out original to find the aberrant code or missing switches.  

You would add an overloaded procedure when argument count needs change. 
 

(*  procedure SaveProjectNames(aProjNames: TProjectNames); overload;
  begin
    xProjectNames := aProjNames;
  end;
*)

 

  • Thanks 1

Share this post


Link to post

 

Not a fancy solution, but have you considered using a List?
You can load all your types ( or only the required ones) with a simple loop.

 

  lPrjList := TList<TProjectType>.Create;
  lPrjList.AddRange([ ptMain, ptExternal, ptDivision, ptBranch  ]);


You can set the "list" with the types you need and pass it as required.
Anyway, you no longer would require to check your code for "case" or keep track of "projecttype" changes. Just let the "process" add the required project type in the list and a routine would deal with what's inside.
If you need some extra boost, you could link each projectType to a specific method to get called ( or a Class, or an interface ) by using a dictionary (or another more suitable data structure).

 

Anyway, my $0.02
 

 

Edited by Clément
  • Like 1

Share this post


Link to post

Some very unique suggestions and probably very useful if it was just a single case of enum. Was looking for general solution I can apply to many enums (I guess I should have mentioned that in first post).

 

Here is example where the connection gets lost:

 

image.thumb.png.7d81efdb7c13d912db13e9f47a023dd8.png

 

And setting projects filter with:

vDataFilter.Projects([cbI.Checked, cbE.Checked, cbD.Checked, cbB.Checked, cbC.Checked, cbS.Checked, cbCity.Checked]);

And then the conversion from open array to array[TProjectType] of string/boolean.. occurs.

 

So, I'm not looking to redesign UI, just thought perhaps there was simple solution that will trigger alert to add new checkbox and its state as filter.

 

Share this post


Link to post

Assuming the Tag value of each checkbox can be set to the ordinal of it's enum, you could walk the enums and check if you find a checkbox under Filters for each of them.

If you don't find it - you change the caption of "Filters: to "Filters is INCOMPLETE" or log it or whatever.

 

But - it would be runtime, preferably in the debug code.  Likewise, you could easily take a set of enum values to get or set the states of the check boxes.

It is a little inconvenient to do this in Generics, since we don't have a constraint for enumerated types - which means we can't really use Low, High or Ord, but it is possible.

 

 

 

 

  • Like 1

Share this post


Link to post

Compiler directives come to help.

This is to ensure TEnum is starting from 1 (needed to loop through a string that contains chars for every item in enum)

const
  EnumLiterals: array[TEnum] of Char = ('Q', 'W', 'E', 'R', 'T');

function SetToFixedStr(aSet: TSomeSet): string;
var elem: TEnum;
begin
  // check
  {$IF Ord(Low(TEnum)) <> 1} {$MESSAGE FATAL 'Must start from 1 (using as string index)'} {$IFEND}
  ...
end

Or, in every case that handles ALL values in enum:

  case item of
    enumItem1:
      ...;
    enumItemN:      
	  ...;
    {$IF Ord(High(TEnum)) <> 5} {$MESSAGE FATAL 'Implement it'} {$IFEND}
  end; // case

This will catch addition to every place of enum. In case of enums starting from custom number, use Ord(High(TStatsType))-Ord(Low(TStatsType)) to get number of items. Btw, FPC catches switches that don't handle every item in an enum. Sometimes it helps, sometimes annoys 🙂

 

Edited by Fr0sT.Brutal
  • Like 1
  • Thanks 1

Share this post


Link to post
Quote

So, I'm not looking to redesign UI, just thought perhaps there was simple solution that will trigger alert to add new checkbox and its state as filter.

I have switched to reading the header line of the data table coming in. So if data table has five fields the reads five fields. A data table with six fields reads the six 'names' through iteration.  Controls are named and

labeled with these 'names'. Hard coding enums for each column required rework as extra data table fields are added.  How does your report handle datetime and time period if I may ask? 

  • Like 1

Share this post


Link to post
3 hours ago, Pat Foley said:

I have switched to reading the header line of the data table coming in. So if data table has five fields the reads five fields. A data table with six fields reads the six 'names' through iteration.  Controls are named and

labeled with these 'names'. Hard coding enums for each column required rework as extra data table fields are added.  How does your report handle datetime and time period if I may ask? 

Lost of my reports are from 15+ years ago, so not using the best approach in design. Datetime and time are not used as filters, so no worries there.

I'm not too experienced with dynamically cretingg UI controls based on data, so filters are definitely manual work and data always shown in virtual treeview.

 

 

3 hours ago, Fr0sT.Brutal said:

{$IF Ord(High(TEnum)) <> 5} {$MESSAGE FATAL 'Implement it'} {$IFEND}

I didn't know about this $MESSAGE FATAL. It's almost like Assert for runtime. Pretty cool!

Edited by Mike Torrettinni

Share this post


Link to post
1 hour ago, Mike Torrettinni said:

I didn't know about this $MESSAGE FATAL. It's almost like Assert for runtime. Pretty cool!

Yep and they're with us since D2009 or so. Even in D7 there were $message (without modificator). Though, it could be just a plain text or anything else:

{$IF ...} BADABOOM! {$IFEND} but this makes source parsing harder (like generating docs) so $message is preferred.

I also add such kind of checks to places where I rely on some assumptions. F.ex., I send short 4-char string code via PostMessage directly in WPARAM so I add compiler directive to check whether the code truly fits in SizeOf(WPARAM). If sometimes the code grows, these checks won't let me forget where I should modify the algo

  • Thanks 1

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×