Storing information about projects has far-reaching goals: analysis of source code and architecture with recommendations for their optimization.

And let’s start with a simple task: extracting information about the contents of the module, in particular – finding and displaying procedures and functions.

Scripts

I took the algorithm used in the CodeHL.pas syntax highlighting module as the basis of the analyzer, which suggests the need to create a universal procedure for parsing. But for now, to my chagrin, I resorted to copy-paste to get the working code for the UpdateProjectList_UpdateUnitProcList () procedure.

procedure UpdateProjectList_UpdateUnitProcList(AIDUnit: integer; AShowProgress: boolean);
// updating information on procedures modules
// AIDUnit - module ID
// AShowProgress - progress display flag
// the settings of the CodeHL module are used in the work
var
   tmpFileName:string;
   tmpStrings: TStringList;
   i: integer;
   j: integer
   tmpWord:string;
   tmpchar:string;
   s1, s2: string;
   tmpIndex: integer;
   tmpText:string;
   tmpBeginCount: integer;
   tmpProcDetected: boolean;
   tmpProcCount: integer;
   tmpDataSet: TDataSet;
begin
   if AShowProgress then
     Progress(0, 0, R('UAPP_MODULE_DATA_UPDATE',UAPP_MODULE_DATA_UPDATE) );
   try
     tmpFileName := Trim( SQLExecute('SELECT project.path || "\script\" || unit.path FROM unit LEFT JOIN project ON project.id = unit.id_project WHERE unit.id=' + IntToStr(AIDUnit)) ) ;
     // the file may not exist, this is not quite normal, but it is not processed here
     if not FileExists(tmpFileName) then
       exit;
     tmpStrings := TStringList.Create;
     // feature of MVDB - the main file must be encoded in UTF-8, and all others must be in ANSI
     if UpperCase( ExtractFileName(tmpFileName) ) = 'SCRIPT.PAS' then
       tmpStrings.LoadFromFile(tmpFileName)
     else
       tmpStrings.LoadFromFileANSI(tmpFileName);
     //
     tmpText := tmpStrings.Text;
     tmpStrings.Clear;
     // create a list of procedures and functions
     tmpBeginCount := 0;
     tmpProcCount := 0;
     tmpProcDetected := False;
     tmpIndex := -1;
     tmpWord := '';
     for i := 1 to Length(tmpText) do
     begin
       tmpChar := tmpText[i];
       if Ord(tmpChar) = 0 then // line terminator may appear at the end of the line.
         break;
       // linear mode - this disables character-by-character analysis until a trigger sequence is detected
       if tmpIndex <> -1 then
       begin
         s1 := CHL_LM_End[tmpIndex];
         // terminating sequence found?
         if copy(tmpText, i, Length(s1)) = s1 then
         begin
           // if the terminating sequence is more than one character, then we make an adjustment
           if Length(s1) > 1 then
             delete(tmpText, i + 1, Length(s1) - 1);
           //
           tmpIndex := -1;
           tmpWord := '';
         end
         else
           tmpWord := tmpWord + tmpChar;
         continue;
       end;
       // end of word
       if tmpText[i] in [' ', '{', '}', '/', '\', ')', '(', ';', ':', '"', '''' , '.', ',', chr(13)] then
       begin
         // check for start of linear mode
         for j := 0 to Length(CHL_LM_Begin) - 1 do
         begin
           if copy(tmpText, i, Length(CHL_LM_Begin[j])) = CHL_LM_Begin[j] then
           begin
             tmpIndex := j;
             continue;
           end;
         end;
         // check for words
         tmpWord := Trim(tmpWord);
         if tmpWord <> '' then
         begin
           if tmpProcDetected then
           begin
             tmpStrings.Add(tmpWord);
             tmpProcDetected := False;
           end
           else
           if (UpperCase(tmpWord) = 'BEGIN') or (UpperCase(tmpWord) = 'CASE') or (UpperCase(tmpWord) = 'TRY') then
             inc(tmpBeginCount)
           else
           if UpperCase(tmpWord) = 'END' then
           begin
             dec(tmpBeginCount);
             if tmpBeginCount = 0 then
               dec(tmpProcCount);
           end
           else
           if ( UpperCase(tmpWord) = 'PROCEDURE' ) or ( UpperCase(tmpWord) = 'FUNCTION' ) then
           begin
             if tmpProcCount = 0 then
               tmpProcDetected := True;
             inc(tmpProcCount);
           end;
           //
           tmpWord := '';
         end;
       end
       else
         tmpWord := tmpWord + tmpChar;
     end;    
     // see if anything has been deleted...
     Application.ProcessMessages;
     SQLQuery('SELECT id, name FROM unitProc WHERE id_unit = '+IntToStr(AIDUnit),tmpDataSet);
     while not tmpDataSet.EOF do
     begin
       if tmpStrings.IndexOf( tmpDataSet.FieldByName('name').asString ) < 0 then
       begin
         SQLExecute('UPDATE unitProc SET status = "Not found" WHERE id = '+tmpDataSet.FieldByName('id').asString );
         Application.ProcessMessages;
       end;
       tmpDataSet.Next;
     end;
     // see if we need to add...
     for i:=0 to tmpStrings.Count - 1 do
     begin
       if AShowProgress then
         Progress(i, tmpStrings.Count - 1, 'Adding procedures' )
       else
         Application.ProcessMessages;
       tmpDataSet.First;
       tmpProcDetected := False;
       while not tmpDataSet.EOF do
       begin
         if tmpStrings.strings(i) = tmpDataSet.FieldByName('name').asString then
         begin
           tmpProcDetected := True;
           break;
         end;
         tmpDataSet.Next;
       end;
       if not tmpProcDetected then
       begin // add...
         SQLExecute('INSERT INTO unitProc (id_unit,name,status) VALUES ('+IntToStr(AIDUnit)+','+StrToSQL( tmpStrings.strings(i) )+','+StrToSQL('New ')+')' );
         Application.ProcessMessages;
       end;
     end;
     tmpDataSet.Free;
     tmpStrings.Free;
   finally
     if AShowProgress then
       progress();
   end;
end;Code language: Delphi (delphi)

Unlike other filling and checking procedures, only one query to the database is used to retrieve data, and checks are carried out using the TDataSet and TStringList components, in which information about existing records and information about the procedures and functions of the module are loaded.

Another feature of My Visual Database is that the encoding of the main script.pas module must be UTF-8, and the encodings of other modules must be ANSY, and this must be taken into account when parsing module files.

To get the procedure text by name, the UserApp_GetProcBody() function is used, which is based on the same parser as in the UpdateProjectList_UpdateUnitProcList() and CodeHL_TextToHTML() procedures. (I undertake to figure out how to get rid of copy-paste in these three procedures for the next version).

function UserApp_GetProcBody( AText:string; AProcName:string ):string;
var
  i: integer;
  j: integer;
  tmpWord: string;
  tmpChar: string;
  s1, s2: string;
  tmpIndex: integer;
  tmpBeginCount: integer;
  tmpProcDetected: boolean;
  tmpProcCount: integer;
  tmpProcStartPos: integer;
  tmpProcEndPos: integer;
  tmpStartPos: integer;
  tmpBody: boolean;
  tmpText:string;
  s: string;
begin
  tmpText := AText;
  tmpBeginCount := 0;
  tmpProcCount := 0;
  tmpProcDetected := False;
  tmpIndex := -1;
  tmpWord := '';
  tmpBody := False;
  for i := 1 to Length(tmpText) do
   begin
     tmpChar := tmpText[i];
     if Ord(tmpChar) = 0 then // line terminator may appear at the end of the line.
       break;
     if tmpBody then
       Result := Result + tmpChar;
     // linear mode - this disables character-by-character analysis until a trigger sequence is detected
     if tmpIndex <> -1 then
     begin
       s1 := CHL_LM_End[tmpIndex];
       // terminating sequence found?
       if copy(tmpText, i, Length(s1)) = s1 then
       begin
         // if the terminating sequence is more than one character, then we make an adjustment
         if Length(s1) > 1 then
           delete(tmpText, i + 1, Length(s1) - 1);
         //
         tmpIndex := -1;
         tmpWord := '';
       end
       else
         tmpWord := tmpWord + tmpChar;
       continue;
     end;
     // end of word
     if tmpText[i] in [' ', '{', '}', '/', '\', ')', '(', ';', ':', '"', '''' , '.', ',', chr(13)] then
     begin
       // check for start of linear mode
       for j := 0 to Length(CHL_LM_Begin) - 1 do
       begin
         if copy(tmpText, i, Length(CHL_LM_Begin[j])) = CHL_LM_Begin[j] then
         begin
           tmpIndex := j;
           continue;
         end;
       end;
       // check for words
      tmpWord := Trim(tmpWord);
      if tmpWord <> '' then
      begin
        if tmpProcDetected then
        begin
          if UpperCase(tmpWord) = UpperCase(AProcName) then
          begin
            for j := i-Length(tmpWord)-9 downto 0 do
            begin
              s := copy(tmpText,j,i-j+1);
              if ( UpperCase(copy(s,1,9)) = 'PROCEDURE') or ( UpperCase(copy(s,1,8 )) = 'FUNCTION') then
              begin
                Result := s;
                break;
              end;
            end;
            tmpBody := True;
          end;
          tmpProcDetected := False;
        end
        else
        if (UpperCase(tmpWord) = 'BEGIN') or (UpperCase(tmpWord) = 'CASE')  or (UpperCase(tmpWord) = 'TRY') then
          inc(tmpBeginCount)
        else
        if UpperCase(tmpWord) = 'END' then
        begin
          dec(tmpBeginCount);
          if tmpBeginCount = 0 then
          begin
            dec(tmpProcCount);
            if (tmpProcCount = 0) and tmpBody then
              tmpBody := False;
          end;
        end
        else
        if  ( UpperCase(tmpWord) = 'PROCEDURE' ) or ( UpperCase(tmpWord) = 'FUNCTION' ) then
        begin
          if tmpProcCount = 0 then
            tmpProcDetected := True;
          inc( tmpProcCount );
        end;
        //
        tmpWord := '';
      end;
    end
    else
      tmpWord := tmpWord + tmpChar;
  end;
end;Code language: Delphi (delphi)

Other improvements

  • UserApp_LoadExample() – добавлена обработка случая, когда источником примера является таблица отображения процедур модуля: для извлечения текста вызывается UserApp_GetProcBody().
  • Splash_Create() – добавлена строка для отображения статуса инициализации приложения.
  • Splash_ShowProgress() – добавлена процедура отображения статуса инициализации приложения. Актуально для приложений с большим временем запуска.
  • UserApp_InitForm() – добавлены вызовы Splash_ShowProgress() для пояснения этапов инициализации
  • UserApp_UpdateDD() – добавлена обработка для формы dtfUnit_Tree, у которой теперь есть дочерняя форма dtfUnitProc
  • frmMain_btnProc_Update_OnClick() – добавлена процедура обработки нажатия кнопки, по которой производится анализ выбранного модуля.
  • english.txt, русский.txt – в файлы локализации ресурсов добавлены новые данные

Result

I changed the picture on the splash screen to make the inscriptions more readable.

Added a PageControl to display procedures and functions (in the future I plan to collect information about constants and global variables).

When you select a procedure, you can view its code.

Descriptions for procedures built into My Visual Database have been added to the database.

Plans

  • Display information about application static forms (forms created in the My Visual Database form editor).
  • Display information about the structure of the database
  • Show dependencies between modules and forms
  • Extract descriptive data from sources to create automatic project documentation.
  • Add to the search engine search for projects, modules and module procedures
  • Synchronization of module versions.
  • Creation of reference modules (repository of modules).

Links

Leave a Reply

Your email address will not be published. Required fields are marked *