diff --git a/IntfExtract/ExtractPascalInterfaces.dpr b/IntfExtract/ExtractPascalInterfaces.dpr new file mode 100644 index 0000000..d5bb411 --- /dev/null +++ b/IntfExtract/ExtractPascalInterfaces.dpr @@ -0,0 +1,991 @@ +program ExtractPascalInterfaces; + +{$APPTYPE CONSOLE} + +uses + System.SysUtils, + System.IOUtils, + System.Classes, + System.Generics.Collections, + System.Generics.Defaults, + System.StrUtils, + System.Types, + System.Character, + Winapi.Windows; // For clipboard access + +type + { Forward declaration for TUnitInfoList needed in TUnitInfo } + TUnitInfo = class; + TUnitInfoList = TList; + + { TUnitInfo holds information about a single Pascal unit } + TUnitInfo = class + private + FOriginalLines: TStringList; + procedure ExtractUsesFromClause(const Clause: string); + function StripCommentsFromUsesBlock(const Block: string): string; + function FindTerminatingSemicolon(const Block: string): Integer; + function CheckAndExtractKeywordContent( + const LineInput: string; + const KeywordUpperCase: string; + out ContentAfterKeyword: string + ): Boolean; + public + Name: string; + NormalizedName: string; + FilePath: string; + InterfaceSection: TStringList; + InterfaceUses: TStringList; + + AdjacencyList: TUnitInfoList; + InDegree: Integer; + Processed: Boolean; + + constructor Create(const AFilePath: string); + destructor Destroy; override; + function ParseUnit: Boolean; + end; + +const + DefaultExcludedDirNames: array[0..6] of string = ('.svn', '.git', '.hg', '__history', '__recovery', '.vs', '.vscode'); + +// Helper procedure to set clipboard text using WinAPI +procedure SetClipboardText(const Text: string); +var + hGlobal: THandle; + pGlobal: Pointer; +begin + if not OpenClipboard(0) then + RaiseLastOSError; + try + EmptyClipboard; + hGlobal := GlobalAlloc(GMEM_MOVEABLE, (Length(Text) + 1) * SizeOf(Char)); + if hGlobal = 0 then + RaiseLastOSError; + + pGlobal := GlobalLock(hGlobal); + if pGlobal = nil then + begin + GlobalFree(hGlobal); + RaiseLastOSError; + end; + + try + Move(PChar(Text)^, pGlobal^, (Length(Text) + 1) * SizeOf(Char)); + finally + GlobalUnlock(hGlobal); + end; + + if SetClipboardData(CF_UNICODETEXT, hGlobal) = 0 then + begin + GlobalFree(hGlobal); // We must free it if SetClipboardData fails + RaiseLastOSError; + end; + // On success, the system owns the memory handle. + finally + CloseClipboard; + end; +end; + + +{ TUnitInfo implementation } +constructor TUnitInfo.Create(const AFilePath: string); +begin + inherited Create; + FilePath := AFilePath; + Name := ''; + NormalizedName := ''; + InterfaceSection := TStringList.Create; + InterfaceUses := TStringList.Create; + FOriginalLines := TStringList.Create; + AdjacencyList := TUnitInfoList.Create; + InDegree := 0; + Processed := False; +end; + +destructor TUnitInfo.Destroy; +begin + InterfaceSection.Free; + InterfaceUses.Free; + FOriginalLines.Free; + AdjacencyList.Free; + inherited Destroy; +end; + +function TUnitInfo.StripCommentsFromUsesBlock(const Block: string): string; +var + Builder: TStringBuilder; + i: Integer; + L: Integer; + inLineComment: Boolean; + inBlockCommentParen: Boolean; + inBlockCommentBrace: Boolean; + currentChar, nextChar: Char; + tempResult: string; +begin + Builder := TStringBuilder.Create; + L := Length(Block); + i := 1; + inLineComment := False; + inBlockCommentParen := False; + inBlockCommentBrace := False; + while i <= L do + begin + currentChar := Block[i]; + if i < L then + nextChar := Block[i + 1] + else + nextChar := #0; + if inLineComment then + begin + if (currentChar = #13) or (currentChar = #10) then + begin + inLineComment := False; + if (Builder.Length > 0) and (Builder[Builder.Length-1] <> ' ') and (Builder[Builder.Length-1] <> ',') then + Builder.Append(' '); + end; + Inc(i); + Continue; + end; + if inBlockCommentParen then + begin + if (currentChar = '*') and (nextChar = ')') then + begin + inBlockCommentParen := False; + Inc(i, 2); + end + else + Inc(i); + Continue; + end; + if inBlockCommentBrace then + begin + if currentChar = '}' then + begin + inBlockCommentBrace := False; + Inc(i); + end + else + Inc(i); + Continue; + end; + if (currentChar = '/') and (nextChar = '/') then + begin + inLineComment := True; + Inc(i, 2); + Continue; + end; + if (currentChar = '(') and (nextChar = '*') then + begin + inBlockCommentParen := True; + Inc(i, 2); + Continue; + end; + if currentChar = '{' then + begin + inBlockCommentBrace := True; + Inc(i); + Continue; + end; + if (currentChar = #13) or (currentChar = #10) or (currentChar = #9) then + begin + if (Builder.Length = 0) or ((Builder.Length > 0) and (Builder[Builder.Length-1] <> ' ')) then + Builder.Append(' '); + Inc(i); + end + else + begin + Builder.Append(currentChar); + Inc(i); + end; + end; + tempResult := Trim(Builder.ToString); + Builder.Free; + while Pos(' ', tempResult) > 0 do + tempResult := StringReplace(tempResult, ' ', ' ', [rfReplaceAll]); + tempResult := StringReplace(tempResult, ' ,', ',', [rfReplaceAll]); + tempResult := StringReplace(tempResult, ', ', ',', [rfReplaceAll]); + Result := tempResult; +end; + +procedure TUnitInfo.ExtractUsesFromClause(const Clause: string); +var + parts: TArray; + partName: string; +begin + if Trim(Clause) = '' then + Exit; + parts := Clause.Split([','], TStringSplitOptions.ExcludeEmpty); + for partName in parts do + begin + var pn := Trim(partName); + if pn <> '' then + InterfaceUses.Add(System.SysUtils.LowerCase(pn)); + end; +end; + +function TUnitInfo.FindTerminatingSemicolon(const Block: string): Integer; +var + i: Integer; + L: Integer; + inLineComment: Boolean; + inBlockCommentParen: Boolean; + inBlockCommentBrace: Boolean; + currentChar: Char; + nextChar: Char; +begin + Result := 0; + L := Length(Block); + i := 1; + inLineComment := False; + inBlockCommentParen := False; + inBlockCommentBrace := False; + while i <= L do + begin + currentChar := Block[i]; + if i < L then + nextChar := Block[i + 1] + else + nextChar := #0; + if inLineComment then + begin + if (currentChar = #13) or (currentChar = #10) then + inLineComment := False; + Inc(i); + Continue; + end; + if inBlockCommentParen then + begin + if (currentChar = '*') and (nextChar = ')') then + begin + inBlockCommentParen := False; + Inc(i, 2); + end + else + Inc(i); + Continue; + end; + if inBlockCommentBrace then + begin + if currentChar = '}' then + begin + inBlockCommentBrace := False; + Inc(i); + end + else + Inc(i); + Continue; + end; + if (currentChar = '/') and (nextChar = '/') then + begin + inLineComment := True; + Inc(i, 2); + Continue; + end; + if (currentChar = '(') and (nextChar = '*') then + begin + inBlockCommentParen := True; + Inc(i, 2); + Continue; + end; + if currentChar = '{' then + begin + inBlockCommentBrace := True; + Inc(i); + Continue; + end; + if currentChar = ';' then + begin + Result := i; + Exit; + end; + Inc(i); + end; +end; + +function TUnitInfo.CheckAndExtractKeywordContent( + const LineInput: string; + const KeywordUpperCase: string; + out ContentAfterKeyword: string +): Boolean; +var + trimmedLine: string; + keywordLen: Integer; + charAfterKeyword: Char; +begin + Result := False; + ContentAfterKeyword := ''; + trimmedLine := Trim(LineInput); + keywordLen := Length(KeywordUpperCase); + if Length(trimmedLine) >= keywordLen then + begin + if System.SysUtils.UpperCase(Copy(trimmedLine, 1, keywordLen)) = KeywordUpperCase then + begin + if Length(trimmedLine) = keywordLen then + begin + Result := True; + ContentAfterKeyword := ''; + end + else + begin + charAfterKeyword := trimmedLine[keywordLen + 1]; + if not charAfterKeyword.IsLetterOrDigit then + begin + Result := True; + ContentAfterKeyword := Trim(Copy(trimmedLine, keywordLen + 1, MaxInt)); + end; + end; + end; + end; +end; + +function TUnitInfo.ParseUnit: Boolean; +var + i: Integer; + LineValue: string; + upperLine: string; + inInterface: Boolean; + inUsesBlock: Boolean; + usesBuilder: TStringBuilder; + potentialUnitNameLine: string; + nameExtracted: Boolean; + contentAfterKeyword: string; + accumulatedContent: string; + trueEndOfUsesPos: Integer; + contentToClean: string; + cleanedContent: string; +begin + Result := False; + if not TFile.Exists(FilePath) then + exit; + FOriginalLines.LoadFromFile(FilePath); + inInterface := False; + inUsesBlock := False; + nameExtracted := False; + usesBuilder := TStringBuilder.Create; + try + for i := 0 to FOriginalLines.Count - 1 do + begin + LineValue := FOriginalLines[i]; + upperLine := System.SysUtils.UpperCase(Trim(LineValue)); + + if not nameExtracted and CheckAndExtractKeywordContent(LineValue, 'UNIT', contentAfterKeyword) then + begin + potentialUnitNameLine := contentAfterKeyword; + if Pos(';', potentialUnitNameLine) > 0 then + Name := Trim(Copy(potentialUnitNameLine, 1, Pos(';', potentialUnitNameLine) - 1)) + else + Name := Trim(potentialUnitNameLine); + if Name <> '' then + begin + NormalizedName := System.SysUtils.LowerCase(Name); + nameExtracted := True; + end + else + begin + WriteLn('Warning: Could not parse unit name from: ' + LineValue + ' in file ' + FilePath); + exit; + end; + end; + + if not inInterface and upperLine.StartsWith('INTERFACE') then + begin + inInterface := True; + end; + + if inInterface then + begin + if upperLine.StartsWith('IMPLEMENTATION') or upperLine.StartsWith('END.') then + begin + if inUsesBlock and (usesBuilder.Length > 0) then + begin + accumulatedContent := usesBuilder.ToString; + trueEndOfUsesPos := FindTerminatingSemicolon(accumulatedContent); + if trueEndOfUsesPos > 0 then + contentToClean := Trim(Copy(accumulatedContent, 1, trueEndOfUsesPos - 1)) + else + contentToClean := Trim(accumulatedContent); + if contentToClean <> '' then + begin + cleanedContent := StripCommentsFromUsesBlock(contentToClean); + if Trim(cleanedContent) <> '' then + ExtractUsesFromClause(cleanedContent); + end; + end; + Result := True; + exit; + end; + + var lineIsForUses := inUsesBlock; + if not inUsesBlock and CheckAndExtractKeywordContent(LineValue, 'USES', contentAfterKeyword) then + begin + usesBuilder.Clear; + inUsesBlock := True; + usesBuilder.AppendLine(contentAfterKeyword); // Use AppendLine to preserve structure for comment stripping + lineIsForUses := True; + end + else if inUsesBlock then + begin + // FIXED: Use AppendLine to preserve newlines for comment stripping logic + usesBuilder.AppendLine(Trim(LineValue)); + lineIsForUses := True; + end; + + if not upperLine.StartsWith('INTERFACE') and not lineIsForUses then + begin + InterfaceSection.Add(LineValue); + end; + + if inUsesBlock then + begin + accumulatedContent := usesBuilder.ToString; + trueEndOfUsesPos := FindTerminatingSemicolon(accumulatedContent); + if trueEndOfUsesPos > 0 then + begin + contentToClean := Trim(Copy(accumulatedContent, 1, trueEndOfUsesPos - 1)); + usesBuilder.Clear; + inUsesBlock := False; + if contentToClean <> '' then + begin + cleanedContent := StripCommentsFromUsesBlock(contentToClean); + if Trim(cleanedContent) <> '' then + ExtractUsesFromClause(cleanedContent); + end; + end; + end; + end; + end; + if inInterface then + begin + Result := True; + end; + finally + usesBuilder.Free; + end; +end; + +procedure CollectPasFilesRecursively(const Path: string; Files: TStringList; const ExcludedDirPatterns: TStrings); +var + dirName: string; + foundPasFiles: TStringDynArray; + subDirs: TStringDynArray; + i: Integer; + currentSubDir: string; + isExcluded: Boolean; + exclusionPattern: string; +begin + try + foundPasFiles := TDirectory.GetFiles(Path, '*.pas', TSearchOption.soTopDirectoryOnly); + for i := Low(foundPasFiles) to High(foundPasFiles) do + Files.Add(foundPasFiles[i]); + except + on E: EDirectoryNotFoundException do + begin + WriteLn('Warning: Could not access path ' + Path + ' while scanning files: ' + E.Message); + Exit; + end; + on E: EInOutError do + begin + WriteLn('Warning: IO error accessing path ' + Path + ' while scanning files: ' + E.Message); + Exit; + end; + end; + try + subDirs := TDirectory.GetDirectories(Path); + except + on E: EDirectoryNotFoundException do + begin + WriteLn('Warning: Could not access path ' + Path + ' while scanning subdirectories: ' + E.Message); + Exit; + end; + on E: EInOutError do + begin + WriteLn('Warning: IO error accessing path ' + Path + ' while scanning subdirectories: ' + E.Message); + Exit; + end; + end; + for currentSubDir in subDirs do + begin + dirName := ExtractFileName(currentSubDir); + isExcluded := False; + for exclusionPattern in ExcludedDirPatterns do + begin + if SameText(dirName, exclusionPattern) then + begin + isExcluded := True; + Break; + end; + end; + if not isExcluded then + CollectPasFilesRecursively(currentSubDir, Files, ExcludedDirPatterns); + end; +end; + +var + allUnits: TDictionary; + sortedUnits: TList; + sourceDirectories: TList; + processingQueue: TQueue; + excludedDirectoryPatternsList: TStringList; + allFoundPasFilesList: TStringList; + item: TUnitInfo; + outputLineText: string; + recursiveSearch: Boolean; + outputFileName: string; + writeToClipboard: Boolean; + currentParamIndex: Integer; + focusMode: Boolean; + focusFilesParameter: string; + initialFocusFiles: TStringList; + filesToExcludeFromOutput: TStringList; + +begin + recursiveSearch := False; + outputFileName := ''; + writeToClipboard := False; + sourceDirectories := nil; + allUnits := nil; + sortedUnits := nil; + processingQueue := nil; + excludedDirectoryPatternsList := nil; + allFoundPasFilesList := nil; + initialFocusFiles := nil; + filesToExcludeFromOutput := nil; + exitcode := 0; + focusMode := False; + focusFilesParameter := ''; + + try + var usageStr := 'Usage: ExtractPascalInterfaces.exe [-r] [-dirs ] [-f | -fx ] [-o | -c] ...'; + + WriteLn('Delphi Interface Extractor'); + sourceDirectories := TList.Create; + excludedDirectoryPatternsList := TStringList.Create; + initialFocusFiles := TStringList.Create; + filesToExcludeFromOutput := TStringList.Create; + for var excludedName in DefaultExcludedDirNames do + excludedDirectoryPatternsList.Add(excludedName); + + currentParamIndex := 1; + while currentParamIndex <= ParamCount do + begin + var currentParam := ParamStr(currentParamIndex); + + if SameText(currentParam, '-r') then + begin + recursiveSearch := True; + Inc(currentParamIndex); + end + else if SameText(currentParam, '-c') then + begin + writeToClipboard := True; + Inc(currentParamIndex); + end + else if SameText(currentParam, '-o') then + begin + if currentParamIndex + 1 <= ParamCount then + begin + outputFileName := ParamStr(currentParamIndex + 1); + Inc(currentParamIndex, 2); + end + else begin WriteLn('Error: Missing filename after -o option.'); WriteLn(usageStr); exitcode := 1; exit; end; + end + else if SameText(currentParam, '-dirs') then + begin + if currentParamIndex + 1 <= ParamCount then + begin + var dirsFileName := ParamStr(currentParamIndex + 1); + if TFile.Exists(dirsFileName) then + begin + var dirsFromFile := TStringList.Create; + try + dirsFromFile.LoadFromFile(dirsFileName); + for var dirPath in dirsFromFile do + sourceDirectories.Add(dirPath); + finally + dirsFromFile.Free; + end; + end + else + WriteLn('Warning: File specified with -dirs not found: ' + dirsFileName); + Inc(currentParamIndex, 2); + end + else begin WriteLn('Error: Missing filename after -dirs option.'); WriteLn(usageStr); exitcode := 1; exit; end; + end + else if SameText(currentParam, '-f') or SameText(currentParam, '-fx') then + begin + if focusMode then + begin WriteLn('Error: -f and -fx options cannot be used together.'); WriteLn(usageStr); exitcode := 1; exit; end; + + if currentParamIndex + 1 <= ParamCount then + begin + focusMode := True; + focusFilesParameter := ParamStr(currentParamIndex + 1); + initialFocusFiles.Text := StringReplace(focusFilesParameter, ';', sLineBreak, [rfReplaceAll]); + if SameText(currentParam, '-fx') then + filesToExcludeFromOutput.Text := initialFocusFiles.Text; + Inc(currentParamIndex, 2); + end + else begin WriteLn('Error: Missing file list after ' + currentParam + ' option.'); WriteLn(usageStr); exitcode := 1; exit; end; + end + else + begin + sourceDirectories.Add(currentParam); + inc( currentParamIndex ); + end; + end; + + var i := sourceDirectories.Count - 1; + while i >= 0 do + begin + var trimmedPath := Trim(sourceDirectories[i]); + if (trimmedPath = '') or (not TDirectory.Exists(trimmedPath)) then + begin + if trimmedPath <> '' then + WriteLn('Warning: Directory "' + trimmedPath + '" not found or is not a directory. Skipping.'); + sourceDirectories.Delete(i); + end + else + sourceDirectories[i] := trimmedPath; + Dec(i); + end; + + + if writeToClipboard and (outputFileName <> '') then + begin + WriteLn('Error: -o (output to file) and -c (output to clipboard) cannot be used together.'); + exitcode := 1; + exit; + end; + + if sourceDirectories.Count = 0 then + begin + WriteLn('Error: No source directories specified.'); + WriteLn(usageStr); + exitcode := 1; + exit; + end; + + allFoundPasFilesList := TStringList.Create; + WriteLn(IfThen(recursiveSearch, 'Recursive search enabled.', 'Non-recursive search.')); + + for var currentScanDir in sourceDirectories do + begin + WriteLn('Scanning directory: ' + currentScanDir + IfThen(recursiveSearch, ' (recursively)', '')); + if recursiveSearch then + CollectPasFilesRecursively(currentScanDir, allFoundPasFilesList, excludedDirectoryPatternsList) + else + begin + var filesInDir := TDirectory.GetFiles(currentScanDir, '*.pas', TSearchOption.soTopDirectoryOnly); + for var fName in filesInDir do + allFoundPasFilesList.Add(fName); + end; + end; + + if focusMode then + begin + for var focusFile in initialFocusFiles do + begin + if not TFile.Exists(focusFile) then + begin WriteLn('Warning: Focus file not found, skipping: ' + focusFile); Continue; end; + if allFoundPasFilesList.IndexOf(focusFile) = -1 then + allFoundPasFilesList.Add(focusFile); + end; + end; + + if allFoundPasFilesList.Count = 0 then + begin WriteLn('No .pas files found in any specified directory (respecting exclusions).'); exit; end; + + allUnits := TDictionary.Create(TStringComparer.Ordinal); + sortedUnits := TList.Create; + processingQueue := TQueue.Create; + + WriteLn('Found ' + IntToStr(allFoundPasFilesList.Count) + ' .pas files in total. Parsing...'); + for var filePathKey in allFoundPasFilesList do + begin + var unitInfo := TUnitInfo.Create(filePathKey); + if unitInfo.ParseUnit then + begin + if unitInfo.Name <> '' then + begin + if not allUnits.ContainsKey(unitInfo.NormalizedName) then + allUnits.Add(unitInfo.NormalizedName, unitInfo) + else + begin + WriteLn('Warning: Duplicate unit name "' + unitInfo.Name + '" found.'); + WriteLn(' Existing: ' + allUnits[unitInfo.NormalizedName].FilePath); + WriteLn(' Skipped: ' + unitInfo.FilePath); + unitInfo.Free; + end; + end + else + begin WriteLn('Warning: Could not determine unit name for file: ' + filePathKey + '. Skipping.'); unitInfo.Free; end; + end + else + begin WriteLn('Warning: Failed to parse unit: ' + filePathKey + '. Skipping.'); unitInfo.Free; end; + end; + + if allUnits.Count = 0 then + begin WriteLn('No units successfully parsed.'); exit; end; + + if focusMode then + begin + WriteLn('Focus mode active. Finding dependency cone for files: ' + focusFilesParameter); + var workQueue := TQueue.Create; + var requiredUnits := TDictionary.Create(TStringComparer.Ordinal); + var unitsToRemove := TStringList.Create; + try + for var unitInfo in allUnits.Values do + begin + if initialFocusFiles.IndexOf(unitInfo.FilePath) > -1 then + begin + if not requiredUnits.ContainsKey(unitInfo.NormalizedName) then + begin + workQueue.Enqueue(unitInfo); + requiredUnits.Add(unitInfo.NormalizedName, unitInfo); + end; + end; + end; + + if requiredUnits.Count = 0 then + WriteLn('Warning: None of the initial focus files could be parsed successfully.'); + + while workQueue.Count > 0 do + begin + var currentUnit := workQueue.Dequeue; + for var usedUnitName in currentUnit.InterfaceUses do + begin + var dependencyUnit: TUnitInfo; + if allUnits.TryGetValue(usedUnitName, dependencyUnit) and (not requiredUnits.ContainsKey(usedUnitName)) then + begin + requiredUnits.Add(dependencyUnit.NormalizedName, dependencyUnit); + workQueue.Enqueue(dependencyUnit); + end; + end; + end; + + WriteLn('Found ' + requiredUnits.Count.ToString + ' required units in total.'); + for var unitInfo in allUnits.Values do + if not requiredUnits.ContainsKey(unitInfo.NormalizedName) then + unitsToRemove.Add(unitInfo.NormalizedName); + + if unitsToRemove.Count > 0 then + begin + WriteLn('Filtering out ' + unitsToRemove.Count.ToString + ' unneeded units...'); + for var unitNameToRemove in unitsToRemove do + begin + allUnits[unitNameToRemove].Free; + allUnits.Remove(unitNameToRemove); + end; + end; + + finally + workQueue.Free; + requiredUnits.Free; + unitsToRemove.Free; + end; + end; + + WriteLn('Building dependency graph...'); + for var currentUnit in allUnits.Values do + for var usedUnitName in currentUnit.InterfaceUses do + begin + var usedUnitInfo: TUnitInfo; + if allUnits.TryGetValue(usedUnitName, usedUnitInfo) then + begin + currentUnit.InDegree := currentUnit.InDegree + 1; + usedUnitInfo.AdjacencyList.Add(currentUnit); + end; + end; + + WriteLn(''); + WriteLn('--------------------------------------------------------------------------------'); + WriteLn('Dependency Graph (Project Units Only):'); + WriteLn('--------------------------------------------------------------------------------'); + if allUnits.Count = 0 then + WriteLn('(No units to display in graph)') + else + begin + var tempUnitListForDisplay: TList; + tempUnitListForDisplay := TList.Create(allUnits.Values); + try + tempUnitListForDisplay.Sort( + TComparer + .Construct(function(const L, R: TUnitInfo): Integer begin Result := CompareText(L.Name, R.Name); end) + ); + for var unitToDisplay in tempUnitListForDisplay do + begin + var usesClauseText := ''; + var projectDependenciesFound := False; + if unitToDisplay.InterfaceUses.Count > 0 then + begin + var usedUnitDisplayNames := TStringList.Create; + try + for var usedNormalizedName in unitToDisplay.InterfaceUses do + begin + var usedUnitActualInfo: TUnitInfo; + if allUnits.TryGetValue(usedNormalizedName, usedUnitActualInfo) then + begin + usedUnitDisplayNames.Add(usedUnitActualInfo.Name); + projectDependenciesFound := True; + end; + end; + if projectDependenciesFound then + usesClauseText := usedUnitDisplayNames.CommaText + else + usesClauseText := '(none)'; + finally + usedUnitDisplayNames.Free; + end; + end + else + usesClauseText := '(none)'; + WriteLn(Format(' %-40s -> %s', [unitToDisplay.Name, usesClauseText])); + end; + finally + tempUnitListForDisplay.Free; + end; + end; + WriteLn('--------------------------------------------------------------------------------'); + WriteLn(''); + + WriteLn('Performing topological sort...'); + for item in allUnits.Values do + if item.InDegree = 0 then + processingQueue.Enqueue(item); + while processingQueue.Count > 0 do + begin + var u := processingQueue.Dequeue; + sortedUnits.Add(u); + u.Processed := True; + for var v in u.AdjacencyList do + begin + v.InDegree := v.InDegree - 1; + if (v.InDegree = 0) and (not v.Processed) then + processingQueue.Enqueue(v); + end; + end; + + if sortedUnits.Count < allUnits.Count then + begin + WriteLn('Error: Circular dependency detected among units. Output generation aborted.'); + WriteLn('Units not processed:'); + for item in allUnits.Values do + if not item.Processed then + WriteLn('- ' + item.Name + ' (InDegree: ' + item.InDegree.ToString + ', File: ' + item.FilePath + ')'); + exitcode := 1; + end + else + begin + var outputContent := TStringList.Create; + var externalUses := TDictionary.Create; + try + outputContent.Add('// Combined interfaces from directories:'); + for var dirPath in sourceDirectories do + outputContent.Add('// - ' + dirPath + IfThen(recursiveSearch, ' (recursive)', '')); + if focusMode then + begin + var modeStr := IfThen(filesToExcludeFromOutput.Count > 0, '-fx', '-f'); + outputContent.Add('// Focus mode (' + modeStr + ') active for files: ' + focusFilesParameter); + end; + outputContent.Add('// Generated on: ' + DateTimeToStr(Now)); + outputContent.Add(''); + + for item in sortedUnits do + begin + if filesToExcludeFromOutput.IndexOf(item.FilePath) > -1 then + Continue; + + for var usedUnit in item.InterfaceUses do + if not allUnits.ContainsKey(usedUnit) then + externalUses.AddOrSetValue(usedUnit, True); + end; + + if externalUses.Count > 0 then + begin + var externalUsesList := TStringList.Create; + try + for var extUse in externalUses.Keys do + externalUsesList.Add(extUse); + externalUsesList.Sort; + outputContent.Add('uses'); + for var j := 0 to externalUsesList.Count - 1 do + begin + if j = externalUsesList.Count - 1 then + outputContent.Add(' ' + externalUsesList[j] + ';') + else + outputContent.Add(' ' + externalUsesList[j] + ','); + end; + outputContent.Add(''); + finally + externalUsesList.Free; + end; + end; + + for item in sortedUnits do + begin + if filesToExcludeFromOutput.IndexOf(item.FilePath) > -1 then + Continue; + + if item.InterfaceSection.Count > 0 then + begin + outputContent.Add('//=================================================================================================='); + outputContent.Add('//== UNIT START: ' + item.Name + ' (from ' + ExtractFileName(item.FilePath) + ')'); + outputContent.Add('//=================================================================================================='); + outputContent.AddStrings(item.InterfaceSection); + outputContent.Add('//== UNIT END: ' + item.Name); + outputContent.Add('//=================================================================================================='); + outputContent.Add(''); + end; + end; + + if writeToClipboard then + begin + SetClipboardText(outputContent.Text); + WriteLn('Output successfully copied to clipboard.'); + end + else if outputFileName <> '' then + begin + outputContent.SaveToFile(outputFileName); + WriteLn('Output successfully written to: ' + outputFileName); + end + else + begin + for outputLineText in outputContent do + WriteLn(outputLineText); + end; + finally + outputContent.Free; + externalUses.Free; + end; + end; + except + on E: Exception do + begin + WriteLn('An error occurred: ' + E.ClassName + ': ' + E.Message); + exitcode := 1; + end; + end; + if Assigned(filesToExcludeFromOutput) then + filesToExcludeFromOutput.Free; + if Assigned(initialFocusFiles) then + initialFocusFiles.Free; + if Assigned(excludedDirectoryPatternsList) then + excludedDirectoryPatternsList.Free; + if Assigned(allFoundPasFilesList) then + allFoundPasFilesList.Free; + if Assigned(sourceDirectories) then + sourceDirectories.Free; + if Assigned(allUnits) then + begin + for item in allUnits.Values do + item.Free; + allUnits.Free; + end; + if Assigned(sortedUnits) then + sortedUnits.Free; + if Assigned(processingQueue) then + processingQueue.Free; + if exitcode <> 0 then + WriteLn('Extraction failed.'); +end. diff --git a/IntfExtract/ExtractPascalInterfaces.dproj b/IntfExtract/ExtractPascalInterfaces.dproj new file mode 100644 index 0000000..b58b1d0 --- /dev/null +++ b/IntfExtract/ExtractPascalInterfaces.dproj @@ -0,0 +1,1123 @@ + + + {5DAD7134-DF88-4C8E-B509-BEE80250F617} + 20.3 + None + True + Debug + Win64 + ExtractPascalInterfaces + 3 + Console + ExtractPascalInterfaces.dpr + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_1 + true + true + + + true + Cfg_1 + true + true + + + true + Base + true + + + .\$(Platform)\$(Config) + .\$(Platform)\$(Config) + false + false + false + false + false + System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) + ExtractPascalInterfaces + + + vclwinx;fmx;vclie;DbxCommonDriver;bindengine;IndyIPCommon;VCLRESTComponents;FireDACCommonODBC;FireDACCommonDriver;IndyProtocols;vclx;Skia.Package.RTL;IndyIPClient;dbxcds;vcledge;bindcompvclwinx;FmxTeeUI;bindcompfmx;inetdb;FireDACSqliteDriver;DbxClientDriver;Tee;soapmidas;vclactnband;TeeUI;fmxFireDAC;dbexpress;DBXMySQLDriver;VclSmp;inet;vcltouch;fmxase;dbrtl;Skia.Package.FMX;fmxdae;TeeDB;FireDACMSAccDriver;CustomIPTransport;vcldsnap;DBXInterBaseDriver;IndySystem;Skia.Package.VCL;vcldb;vclFireDAC;bindcomp;FireDACCommon;inetstn;IndyCore;RESTBackendComponents;bindcompdbx;rtl;FireDACMySQLDriver;FireDACADSDriver;RESTComponents;DBXSqliteDriver;vcl;IndyIPServer;dsnapxml;dsnapcon;adortl;vclimg;FireDACPgDriver;FireDAC;inetdbxpress;xmlrtl;tethering;bindcompvcl;dsnap;CloudService;fmxobj;bindcompvclsmp;FMXTee;soaprtl;soapserver;FireDACIBDriver;$(DCC_UsePackage) + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + Debug + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + true + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png + + + vclwinx;fmx;vclie;DbxCommonDriver;bindengine;IndyIPCommon;VCLRESTComponents;FireDACCommonODBC;FireDACCommonDriver;IndyProtocols;vclx;IndyIPClient;dbxcds;vcledge;bindcompvclwinx;FmxTeeUI;bindcompfmx;inetdb;FireDACSqliteDriver;DbxClientDriver;Tee;soapmidas;vclactnband;TeeUI;fmxFireDAC;dbexpress;DBXMySQLDriver;VclSmp;inet;vcltouch;fmxase;dbrtl;fmxdae;TeeDB;FireDACMSAccDriver;CustomIPTransport;vcldsnap;DBXInterBaseDriver;IndySystem;Skia.Package.VCL;vcldb;vclFireDAC;bindcomp;FireDACCommon;inetstn;IndyCore;RESTBackendComponents;bindcompdbx;rtl;FireDACMySQLDriver;FireDACADSDriver;RESTComponents;DBXSqliteDriver;vcl;IndyIPServer;dsnapxml;dsnapcon;adortl;vclimg;FireDACPgDriver;FireDAC;inetdbxpress;xmlrtl;tethering;bindcompvcl;dsnap;CloudService;fmxobj;bindcompvclsmp;FMXTee;soaprtl;soapserver;FireDACIBDriver;$(DCC_UsePackage) + true + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) + Debug + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + + + DEBUG;$(DCC_Define) + true + false + true + true + true + true + true + + + false + + + -r -o T:\out.txt -dirs T:\Myc\dirs.txt -f "C:\Users\Brummel\Documents\cAlgo\DataExport\ChartDisplay\ChartDisplayMain.pas" + + + false + RELEASE;$(DCC_Define) + 0 + 0 + + + + MainSource + + + + Base + + + Cfg_1 + Base + + + Cfg_2 + Base + + + + Delphi.Personality.12 + Application + + + + ExtractPascalInterfaces.dpr + + + + + + true + + + + + true + + + + + true + + + + + .\ + true + + + + + ExtractPascalInterfaces.exe + true + + + + + ExtractPascalInterfaces.rsm + true + + + + + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + res\xml + 1 + + + res\xml + 1 + + + + + library\lib\armeabi + 1 + + + library\lib\armeabi + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + library\lib\mips + 1 + + + library\lib\mips + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-anydpi-v21 + 1 + + + res\drawable-anydpi-v21 + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\values-v21 + 1 + + + res\values-v21 + 1 + + + + + res\values-v31 + 1 + + + res\values-v31 + 1 + + + + + res\values-v35 + 1 + + + res\values-v35 + 1 + + + + + res\drawable-anydpi-v26 + 1 + + + res\drawable-anydpi-v26 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-anydpi-v33 + 1 + + + res\drawable-anydpi-v33 + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\values-night-v21 + 1 + + + res\values-night-v21 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-ldpi + 1 + + + res\drawable-ldpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-small + 1 + + + res\drawable-small + 1 + + + + + res\drawable-normal + 1 + + + res\drawable-normal + 1 + + + + + res\drawable-large + 1 + + + res\drawable-large + 1 + + + + + res\drawable-xlarge + 1 + + + res\drawable-xlarge + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\drawable-anydpi-v24 + 1 + + + res\drawable-anydpi-v24 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-night-anydpi-v21 + 1 + + + res\drawable-night-anydpi-v21 + 1 + + + + + res\drawable-anydpi-v31 + 1 + + + res\drawable-anydpi-v31 + 1 + + + + + res\drawable-night-anydpi-v31 + 1 + + + res\drawable-night-anydpi-v31 + 1 + + + + + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + Contents\MacOS + 1 + .framework + + + Contents\MacOS + 1 + .framework + + + Contents\MacOS + 1 + .framework + + + 0 + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + 0 + .dll;.bpl + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + 0 + .bpl + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + Contents\Resources\StartUp\ + 0 + + + Contents\Resources\StartUp\ + 0 + + + Contents\Resources\StartUp\ + 0 + + + 0 + + + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + ..\ + 1 + + + ..\ + 1 + + + ..\ + 1 + + + + + Contents + 1 + + + Contents + 1 + + + Contents + 1 + + + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + Contents\MacOS + 1 + + + Contents\MacOS + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + library\lib\armeabi-v7a + 1 + + + + + 1 + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + ..\ + 1 + + + ..\ + 1 + + + ..\ + 1 + + + + + 1 + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).launchscreen + 64 + + + ..\$(PROJECTNAME).launchscreen + 64 + + + + + 1 + + + 1 + + + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + + + + + + + + + + + + + True + True + + + 12 + + + + + diff --git a/IntfExtract/ExtractPascalInterfaces.res b/IntfExtract/ExtractPascalInterfaces.res new file mode 100644 index 0000000..bf57230 Binary files /dev/null and b/IntfExtract/ExtractPascalInterfaces.res differ diff --git a/IntfExtract/Readme.md b/IntfExtract/Readme.md new file mode 100644 index 0000000..f9f89ce --- /dev/null +++ b/IntfExtract/Readme.md @@ -0,0 +1,66 @@ +### Projektplan: Delphi Interface Extractor + +**Datum:** 13. Juni 2025, 14:34 + +#### Motivation + +Die Notwendigkeit, `interface`-Abschnitte aus einer Vielzahl von Delphi-Units zu extrahieren und in einer einzigen, kompilierbaren Datei zusammenzufassen. Die größte Herausforderung bestand darin, die Abhängigkeiten zwischen den Units korrekt aufzulösen, um eine gültige Kompilierungsreihenfolge zu gewährleisten. + +#### Ziel + +Entwicklung eines robusten Kommandozeilen-Tools mit folgenden Kernfunktionen: +* Rekursives Scannen von Quelltext-Verzeichnissen unter Ausschluss von VCS-Ordnern. +* Topologische Sortierung der Units zur korrekten Auflösung von Abhängigkeiten. +* Ein "Fokus-Modus" (`-f`, `-fx`) zur gezielten Analyse des Abhängigkeitsbaums einer oder mehrerer Start-Units. +* Die Erzeugung einer einzigen, sauberen Ausgabedatei, die eine global konsolidierte `uses`-Klausel nur mit externen Abhängigkeiten enthält. +* Eine Option (`-fx`), um die `interface`-Sektion der Start-Units selbst aus der Ausgabe auszuschließen – ideal für Test-Szenarien. + +#### Ergebnis + +Wir haben das Kommandozeilen-Tool `ExtractPascalInterfaces.exe` erfolgreich entwickelt. Das Programm erfüllt alle gestellten Anforderungen: +* Der Parser verarbeitet Delphi-Units zuverlässig und extrahiert die `interface`-Inhalte, wobei `uses`-Klauseln und Kommentare korrekt behandelt werden. +* Die topologische Sortierung stellt die korrekte Reihenfolge der Units sicher und erkennt zyklische Abhängigkeiten. +* Die Fokus-Modi `-f` und `-fx` ermöglichen eine präzise Steuerung der zu verarbeitenden Units. +* Die generierte Ausgabedatei ist hochgradig optimiert: Sie beginnt mit einer einzigen, formatierten `uses`-Klausel, die alle externen Abhängigkeiten des Projekts zusammenfasst, gefolgt von den `type`- und `const`-Deklarationen in der richtigen Reihenfolge. + +#### Nächste Schritte + +- [ ] **Testen**: Ausgiebige Tests des Tools mit einem großen, realen Delphi-Projekt. +- [ ] **Erweiterung**: Unterstützung für bedingte Kompilierung (`{$IFDEF}`) innerhalb von `uses`-Klauseln evaluieren. +- [ ] **Konfiguration**: Optional eine Konfigurationsdatei (z.B. `.json`) zur Angabe von Pfaden und Optionen anstelle von Kommandozeilen-Parametern ermöglichen. + + +*** + +### Projektplan: Delphi Interface Extractor (Abschluss) + +**Datum:** 13. Juni 2025, 16:05 + +#### Motivation + +Das ursprüngliche Ziel war die Erstellung eines Werkzeugs, das `interface`-Abschnitte aus Delphi-Units extrahiert und zu einer einzigen, kompilierbaren Datei zusammenfügt. Die zentrale Herausforderung war das korrekte Management von Abhängigkeiten und die Erzeugung einer sauberen, wartbaren Ausgabedatei. + +#### Ziel + +Die Entwicklung eines hochflexiblen und robusten Kommandozeilen-Tools zur Automatisierung der Interface-Extraktion. Das Tool sollte einen reichhaltigen Satz an Funktionen bieten: +* Scannen von Verzeichnissen, die direkt, rekursiv (`-r`) oder über eine Datei (`-dirs`) angegeben werden. +* Zwei Fokus-Modi für gezielte Abhängigkeitsanalysen: `-f` (inklusive Start-Dateien) und `-fx` (exklusive Start-Dateien). +* Flexible Ausgabe der Ergebnisse in eine Datei (`-o`), direkt in die Windows-Zwischenablage (`-c`) oder auf die Konsole. +* Erzeugung einer einzigen, sauberen Ausgabedatei, die mit einer globalen, formatierten `uses`-Klausel beginnt, welche nur die wirklich benötigten externen Abhängigkeiten enthält. +* Ein robuster Parser, der auch komplexe `uses`-Klauseln mit Zeilenkommentaren korrekt verarbeitet. + +#### Ergebnis + +Das Kommandozeilen-Tool `ExtractPascalInterfaces.exe` wurde erfolgreich fertiggestellt und hat alle gesetzten Ziele erreicht. Es ist nun ein ausgereiftes Werkzeug für die Build-Automatisierung. +* **Funktionsumfang:** Alle Parameter (`-r`, `-o`, `-c`, `-dirs`, `-f`, `-fx`) sind implementiert und interagieren korrekt miteinander. +* **Robustheit:** Der Parser wurde im Laufe der Entwicklung mehrfach verfeinert und kann nun auch komplexe `uses`-Klauseln mit eingeschobenen Kommentaren zuverlässig verarbeiten. Kritische Bugs bei der String- und Index-Verarbeitung wurden behoben. +* **Ausgabequalität:** Das erzeugte Artefakt ist optimal für die Weiterverwendung: eine einzige Datei mit einer sauberen, alphabetisch sortierten und formatierten `uses`-Klausel an der Spitze, gefolgt von den topologisch sortierten Interface-Deklarationen. +* **Flexibilität:** Die `fx`-Option ermöglicht den Einsatz in Test-Szenarien, bei denen die Abhängigkeiten eines Test-Harness aufgelöst werden, ohne dieses selbst in die Ausgabe aufzunehmen. Die Clipboard-Ausgabe beschleunigt den manuellen Einsatz erheblich. + +#### Nächste Schritte + +Das Projekt ist aus Entwicklungssicht abgeschlossen. Die nächsten logischen Schritte konzentrieren sich auf die Distribution und Anwendung: + +- [ ] **Dokumentation**: Erstellen einer `README.md`-Datei, die alle Kommandozeilen-Optionen mit Beispielen detailliert beschreibt. +- [ ] **Praxistest**: Einsatz des Tools in einem oder mehreren großen, realen Delphi-Projekten, um die Stabilität unter Beweis zu stellen. +- [ ] **Deployment**: Bereitstellung der finalen `.exe`-Datei für den Einsatz in automatisierten Build-Prozessen. \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index beef4e8..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# MycLib - diff --git a/Src/Myc.Trade.DataStream.pas b/Src/Myc.Trade.DataStream.pas index 72101a3..3602f84 100644 --- a/Src/Myc.Trade.DataStream.pas +++ b/Src/Myc.Trade.DataStream.pas @@ -605,13 +605,13 @@ begin FCurrPosInFile := 0; FCurrentData := FDataServer.LoadDataFile(FCurrentFileName); // Forward all future done events, because this means there is new data. - FCurrentData.Done.Subscribe( FHasData ); + FCurrentData.Done.Subscribe(FHasData); FNextFileName := FDataServer.FindNextDataFile(FCurrentFileName); if FNextFileName <> '' then begin FNextData := FDataServer.LoadDataFile(FNextFileName); - FNextData.Done.Subscribe( FHasData ); + FNextData.Done.Subscribe(FHasData); end; end; @@ -640,7 +640,7 @@ begin if FNextFileName <> '' then begin FNextData := FDataServer.LoadDataFile(FNextFileName); - FNextData.Done.Subscribe( FHasData ); + FNextData.Done.Subscribe(FHasData); end; if not FCurrentData.Done.IsSet then diff --git a/Test/MycTests.dproj b/Test/MycTests.dproj index e6dc8ec..871456b 100644 --- a/Test/MycTests.dproj +++ b/Test/MycTests.dproj @@ -65,7 +65,7 @@ --exitbehavior:Pause TESTINSIGHT;$(DCC_Define) TargetOutOfDate - @@ -158,12 +158,7 @@ $(PreBuildEvent)]]> MycTests.dpr - - Embarcadero C++Builder Office 2000 Servers Package - Embarcadero C++Builder Office XP Servers Package - Microsoft Office 2000 Sample Automation Server Wrapper Components - Microsoft Office XP Sample Automation Server Wrapper Components - + @@ -1170,7 +1165,7 @@ $(PreBuildEvent)]]> - T:\DelphiIntfExtract\Win64\Debug\ExtractPascalInterfaces.exe -o T:\Myc\intf.txt T:\Myc\src + T:\Myc\IntfExtract\Win64\Debug\ExtractPascalInterfaces.exe $(SAVE) -o T:\Myc\intf.txt T:\Myc\src False False @@ -1178,7 +1173,7 @@ $(PreBuildEvent)]]> False - T:\DelphiIntfExtract\Win64\Debug\ExtractPascalInterfaces.exe -o T:\Myc\intf.txt T:\Myc\src + T:\Myc\IntfExtract\Win64\Debug\ExtractPascalInterfaces.exe $(SAVE) -o T:\Myc\intf.txt T:\Myc\src False False @@ -1186,7 +1181,7 @@ $(PreBuildEvent)]]> False - T:\DelphiIntfExtract\Win64\Debug\ExtractPascalInterfaces.exe -o T:\Myc\intf.txt T:\Myc\src + T:\Myc\IntfExtract\Win64\Debug\ExtractPascalInterfaces.exe $(SAVE) -o T:\Myc\intf.txt T:\Myc\src False False @@ -1194,7 +1189,7 @@ $(PreBuildEvent)]]> False - T:\DelphiIntfExtract\Win64\Debug\ExtractPascalInterfaces.exe -o T:\Myc\intf.txt T:\Myc\src + T:\Myc\IntfExtract\Win64\Debug\ExtractPascalInterfaces.exe $(SAVE) -o T:\Myc\intf.txt T:\Myc\src False False diff --git a/dirs.txt b/dirs.txt new file mode 100644 index 0000000..e2b15b1 --- /dev/null +++ b/dirs.txt @@ -0,0 +1,2 @@ +T:\Myc\Src +T:\Myc\Test \ No newline at end of file