Forum > Suggestions

Include Informative Line Count Statistics

(1/5) > >>

munair:
For larger projects in particular, it would be helpful if Lazarus had an option to provide line count statistics. Working on a project for 10 years, I started wondering about the line count stats, so I made a simple command line tool that can serve as an example. My project appears to have close to 30k lines of code and I had no idea.

The stat results are also exported to a CSV file.


--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---{  countlines: Counts lines in *.pas, *.pp, *.dpr, and *.lfm files  - PAS/PP/DPR: Non-empty (TotalLines), Code (incl. code with // comments), Comment (TotalLines - CodeLines), Empty  - LFM: Total lines, Empty lines  - Supports comment blocks  - Ignores nested/malformed comments (bad practice)  - Warns on unclosed comment blocks  - Single-pass file reading, CSV output  - Safeguard for empty directories  copyright (C) Frank Hoogerbeets (https://github.com/ssgeos/)  License: FPC modified LGPL Version 2 (https://wiki.lazarus.freepascal.org/FPC_modified_LGPL)}program countlines; uses  SysUtils; type TSource = record  CodeLines: integer;  CommentLines: integer;  EmptyLines: integer;  TotalLines: integer;end; var  Directory: string;  PasFile: TSource;  LfmFile: TSource;  PasFilesTotal: TSource;  LfmFilesTotal: TSource; procedure CountLinesInFile(const FileName: string; var Source: TSource);// count code linesvar  F: TextFile;  Line: string;  InCommentBlock: Boolean;  TrimmedLine: string;  i: Integer;begin  InCommentBlock := False;   AssignFile(F, FileName);  try    Reset(F);    while not EOF(F) do      begin        ReadLn(F, Line);        TrimmedLine := Trim(Line);         if Trim(Line) = '' then          begin            Inc(Source.EmptyLines);            Continue;          end;         Inc(Source.TotalLines);         // Check for single-line // comment        if not InCommentBlock then          begin            i := Pos('//', TrimmedLine);            if i > 0 then              begin                TrimmedLine := Trim(Copy(TrimmedLine, 1, i - 1));                if TrimmedLine = '' then                  Continue;              end;          end;         // Handle multi-line comment blocks        if InCommentBlock then          begin            i := Pos('}', TrimmedLine);            if i = 0 then              i := Pos('*)', TrimmedLine);            if i > 0 then              begin                InCommentBlock := False;                TrimmedLine := Trim(Copy(TrimmedLine, i + 1, Length(TrimmedLine)));                if TrimmedLine = '' then                  Continue;              end            else              Continue;          end;         // Check for start of multi-line comment block        i := Pos('{', TrimmedLine);        if i = 0 then          i := Pos('(*', TrimmedLine);        if i > 0 then          begin            if Pos('}', TrimmedLine) > i then              begin                TrimmedLine := Trim(Copy(TrimmedLine, 1, i - 1) +                                    Copy(TrimmedLine, Pos('}', TrimmedLine) + 1, Length(TrimmedLine)));                if TrimmedLine = '' then                  Continue;              end            else if Pos('*)', TrimmedLine) > i then              begin                TrimmedLine := Trim(Copy(TrimmedLine, 1, i - 1) +                                    Copy(TrimmedLine, Pos('*)', TrimmedLine) + 1, Length(TrimmedLine)));                if TrimmedLine = '' then                  Continue;              end            else              begin                InCommentBlock := True;                TrimmedLine := Trim(Copy(TrimmedLine, 1, i - 1));                if TrimmedLine = '' then                  Continue;              end;          end;         // Count non-empty, non-comment lines        if TrimmedLine <> '' then          Inc(Source.CodeLines);      end;    if InCommentBlock then      Writeln('WARNING: Unclosed comment block in ', FileName);  except    on E: Exception do      Writeln('Error reading file ', FileName, ': ', E.Message);  end;  CloseFile(F);end; procedure CountFilesInDirectory(const Directory: string);var  SearchRec: TSearchRec;  FilePath, Ext: string;begin  // reset totals  PasFilesTotal := default(TSource);  LfmFilesTotal := default(TSource);   if FindFirst(IncludeTrailingPathDelimiter(Directory) + '*.*', faAnyFile, SearchRec) = 0 then    try      repeat        if (SearchRec.Attr and faDirectory) = 0 then          begin            FilePath := IncludeTrailingPathDelimiter(Directory) + SearchRec.Name;            Ext := LowerCase(ExtractFileExt(SearchRec.Name));            // source code units            if (Ext = '.pas') or (Ext = '.pp') or (Ext = '.dpr') then              begin                PasFile := default(TSource);                CountLinesInFile(FilePath, PasFile);                Inc(PasFilesTotal.TotalLines, PasFile.TotalLines);                Inc(PasFilesTotal.EmptyLines, PasFile.EmptyLines);                Inc(PasFilesTotal.CodeLines, PasFile.CodeLines);                PasFile.CommentLines := PasFile.TotalLines - PasFile.CodeLines;                Inc(PasFilesTotal.CommentLines, PasFile.CommentLines);                Writeln(                  'File: ', SearchRec.Name,                  ', Total Lines (non-empty): ', PasFile.TotalLines,                  ', Code Lines: ', PasFile.CodeLines,                  ', Comment Lines: ', PasFile.CommentLines,                  ', Empty Lines: ', PasFile.EmptyLines                );              end            // auto-generated form files            else if Ext = '.lfm' then              begin                LfmFile := default(TSource);                CountLinesInFile(FilePath, LfmFile);                Inc(LfmFilesTotal.TotalLines, LfmFile.TotalLines);                Inc(LfmFilesTotal.EmptyLines, LfmFile.EmptyLines);                Writeln(                  'File: ', SearchRec.Name,                  ', Total Lines: ', LfmFile.TotalLines,                  ', Empty Lines: ', LfmFile.EmptyLines                );              end;          end;      until FindNext(SearchRec) <> 0;    finally      FindClose(SearchRec);    end;end; procedure SaveCountsToCSV(const Directory: string);var  F: TextFile;  SearchRec: TSearchRec;  FilePath, Ext: string;begin  try    AssignFile(F, Directory + '/line_counts.csv');    Rewrite(F);    Writeln(F, 'File,NonEmpty,Code,Comment,Empty');    if FindFirst(IncludeTrailingPathDelimiter(Directory) + '*.*', faAnyFile, SearchRec) = 0 then                        try                                repeat                                  if (SearchRec.Attr and faDirectory) = 0 then                                    begin                                      FilePath := IncludeTrailingPathDelimiter(Directory) + SearchRec.Name;                                      Ext := LowerCase(ExtractFileExt(SearchRec.Name));                                      if Ext = '.pas' then                                        begin                                          PasFile := default(TSource);                                          CountLinesInFile(FilePath, PasFile);                                          Writeln(F, SearchRec.Name + ',' +                                                  IntToStr(PasFile.TotalLines) + ',' +                                                  IntToStr(PasFile.CodeLines) + ',' +                                                  IntToStr(PasFile.TotalLines - PasFile.CodeLines) + ',' +                                                  IntToStr(PasFile.EmptyLines));                                        end                                      else if Ext = '.lfm' then                                        begin                                          LfmFile := default(TSource);                                          CountLinesInFile(FilePath, LfmFile);                                          Writeln(F, SearchRec.Name + ',' +                                                  IntToStr(LfmFile.TotalLines) + ',,,' +                                                  IntToStr(LfmFile.EmptyLines));                                        end;                                    end;                                until FindNext(SearchRec) <> 0;                        finally                                FindClose(SearchRec);                        end;                        Writeln(F, 'Total PAS,' + IntToStr(PasFilesTotal.TotalLines) + ',' +                                        IntToStr(PasFilesTotal.CodeLines) + ',' +                                        IntToStr(PasFilesTotal.CommentLines) + ',' +                                        IntToStr(PasFilesTotal.EmptyLines));                        Writeln(F, 'Total LFM,' + IntToStr(LfmFilesTotal.TotalLines) + ',,,' +                                        IntToStr(LfmFilesTotal.EmptyLines));      if PasFilesTotal.TotalLines > 0 then        begin          Writeln(F);          Writeln(F, '# Comment lines percentage (of non-empty): ', (PasFilesTotal.CommentLines / PasFilesTotal.TotalLines * 100):0:1, '%');          Writeln(F, '# Empty lines percentage (of total): ', (PasFilesTotal.EmptyLines / (PasFilesTotal.TotalLines + PasFilesTotal.EmptyLines) * 100):0:1, '%');        end;      CloseFile(F);  except    on E: Exception do      Writeln('Error writing CSV: ', E.Message);  end;end; begin  if ParamCount > 0 then    Directory := ParamStr(1)  else    Directory := GetCurrentDir;   if not DirectoryExists(Directory) then    begin      Writeln('Error: Directory ', Directory, ' does not exist.');      Halt(1);    end;   Writeln('Counting lines in directory: ', Directory);   CountFilesInDirectory(Directory);  SaveCountsToCSV(Directory);   WriteLn;  Writeln('Total lines in all *.pas files (non-empty): ', PasFilesTotal.TotalLines);  Writeln('Total code lines (non-comment, non-empty): ', PasFilesTotal.CodeLines);  Writeln('Total comment lines: ', PasFilesTotal.CommentLines);  Writeln('Total empty lines: ', PasFilesTotal.EmptyLines);  if PasFilesTotal.TotalLines > 0 then    begin      Writeln('Comment lines percentage (of non-empty): ', (PasFilesTotal.CommentLines / PasFilesTotal.TotalLines * 100):0:1, '%');      Writeln('Empty lines percentage (of total): ', (PasFilesTotal.EmptyLines / (PasFilesTotal.TotalLines + PasFilesTotal.EmptyLines) * 100):0:1, '%');    end;  Writeln('Total lines in all *.lfm files: ', LfmFilesTotal.TotalLines);  Writeln('Total empty lines: ', LfmFilesTotal.EmptyLines);  WriteLn;  WriteLn('Results saved to -> line_counts.csv');end.
There is a package on Github that will install a menu entry in the Lazarus IDE under Project to provide the same metrics.
https://github.com/ssgeos/ProjectMetrics

Thaddy:
Any TStrings descedant, including TMemo.Items, has a count property that equals the number of lines. I would use that.

n7800:

--- Quote from: munair on May 09, 2025, 09:46:10 am ---For larger projects in particular, it would be helpful if Lazarus had an option to provide line count statistics.

--- End quote ---

I've seen a similar topic before: https://forum.lazarus.freepascal.org/index.php?topic=71084

munair:
That's just yesterday. What a coincidence.

JuhaManninen:
A "Project Statistics" entry is needed in the Project menu.
If you want to implement it, a patch would be accepted.

Navigation

[0] Message Index

[#] Next page

Go to full version