diff --git a/soneta-programming/SKILL.md b/soneta-programming/SKILL.md index 6735c3b..0dc387d 100644 --- a/soneta-programming/SKILL.md +++ b/soneta-programming/SKILL.md @@ -17,21 +17,23 @@ Skill zawiera dokumentację fundamentalnych klas logiki biznesowej platformy eno SKILL.md zawiera "duży obraz" - hierarchię klas, thread-safety, kanoniczne wzorce. Po szczegóły konkretnego tematu sięgaj do referencji: -| Temat | Gdzie szukać | -|---|---| -| Hierarchia ORM, Row / Table / Module, klucze, ISessionable | sekcje poniżej | -| Sesje, transakcje, Login, Database, BusApplication, optimistic locking | [references/session-login.md](references/session-login.md) | -| Paczki danych, Datapack, GuidedRow, ExportedRow, synchronizacja, blokady | [references/datapack-guidedrow.md](references/datapack-guidedrow.md) | -| Klasa Context - dane z UI, zaznaczenia, parametry workera | [references/context.md](references/context.md) | -| Klasy parametrów (ContextBase) - filtry, trwałość, InvokeChanged | [references/contextbase.md](references/contextbase.md) | -| Obiekty Worker i Extender - rozszerzenia modelu, akcje w menu Czynności | [references/worker-extender.md](references/worker-extender.md) | -| Serwisy biznesowe (App / Database / Login / Session scope) | [references/services.md](references/services.md) | -| Tłumaczenia (Translate, TranslateIgnore), ILogger, ActSource | [references/translations-logging.md](references/translations-logging.md) | -| Action result zwracany przez worker / extender / Command - raporty, dialogi, nawigacja | [references/action-result.md](references/action-result.md) | -| RowCondition - serwerowe warunki LINQ, filtrowanie SubTable / View / Query | [references/rowcondition.md](references/rowcondition.md) | +| Temat | Gdzie szukać | +|---------------------------------------------------------------------------------------------------|---| +| Hierarchia ORM, Row / Table / Module, klucze, ISessionable | sekcje poniżej | +| Sesje, transakcje, Login, Database, BusApplication, optimistic locking | [references/session-login.md](references/session-login.md) | +| Paczki danych, Datapack, GuidedRow, ExportedRow, synchronizacja, blokady | [references/datapack-guidedrow.md](references/datapack-guidedrow.md) | +| Klasa Context - dane z UI, zaznaczenia, parametry workera | [references/context.md](references/context.md) | +| Klasy parametrów (ContextBase) - filtry, trwałość, InvokeChanged | [references/contextbase.md](references/contextbase.md) | +| Obiekty Worker i Extender - rozszerzenia modelu, akcje w menu Czynności | [references/worker-extender.md](references/worker-extender.md) | +| Serwisy biznesowe (App / Database / Login / Session scope) | [references/services.md](references/services.md) | +| Tłumaczenia (Translate, TranslateIgnore), ILogger, ActSource | [references/translations-logging.md](references/translations-logging.md) | +| Action result zwracany przez worker / extender / Command - raporty, dialogi, nawigacja | [references/action-result.md](references/action-result.md) | +| RowCondition - serwerowe warunki LINQ, filtrowanie SubTable / View / Query | [references/rowcondition.md](references/rowcondition.md) | | ViewInfo - definicja widoków list (folderów), CreateView, klasa Params, powiązanie z viewform.xml | [references/viewinfo.md](references/viewinfo.md) | | Cechy (Features) - tabela Features, typy cech, dostęp typowany/nietypowany, bindowanie w form.xml | [references/features.md](references/features.md) | -| Gotowe wzorce kodu end-to-end (import, CRUD, obsługa błędów) | [references/examples.md](references/examples.md) | +| Gotowe wzorce kodu end-to-end (import, CRUD, obsługa błędów) | [references/examples.md](references/examples.md) | +| Skanowanie pól obiektu biznesowego z DLL (Roslyn MetadataReference) | [references/scan-props.md](references/scan-props.md) | +| Inwentaryzacja modułów i tabel (`*Module` / `*Row` / `*Table`) z DLL | [references/scan-modules.md](references/scan-modules.md) | ## Architektura warstw @@ -313,6 +315,26 @@ using (var session = login.CreateSession(readOnly: false, config: false, name: " Więcej wzorców (kasowanie, obsługa błędów, pełny import end-to-end) - patrz [references/examples.md](references/examples.md). +## Narzędzia pomocnicze + +Skill udostępnia skrypt `scripts/scan-props.csx` (uruchamiany przez `dotnet script`) do odczytu publicznych pól klasy zagnieżdżonej `XxxModule+XxxRecord` ze skompilowanych DLL dodatku — bez ładowania IL do CLR (Roslyn `MetadataReference.CreateFromFile`). Wypisuje również właściwości klasy biznesowej (kalkulowane), Caption/Description oraz rekurencyjnie rozwija pola typu subrow z notacją kropkową. + +```bash +dotnet script ~/.claude/skills/soneta-programming/scripts/scan-props.csx \ + -- DokumentHandlowy ./bin/Debug/net8.0 +``` + +Szczegóły, kody wyjścia i ograniczenia: [references/scan-props.md](references/scan-props.md). + +Drugi skrypt — `scripts/scan-modules.csx` — listuje wszystkie moduły (`*Module` dziedziczące z `Soneta.Business.Module`) oraz znajdujące się w nich tabele (`RowType`/`TableType` z Caption/Description). Pomocne przy wstępnej inwentaryzacji bibliotek, zanim sięgniesz po `scan-props.csx` dla konkretnej tabeli. + +```bash +dotnet script ~/.claude/skills/soneta-programming/scripts/scan-modules.csx \ + -- ./bin/Debug/net8.0 +``` + +Szczegóły: [references/scan-modules.md](references/scan-modules.md). + ## Konwencje nazewnicze | Element | Konwencja | Przykład | diff --git a/soneta-programming/references/scan-modules.md b/soneta-programming/references/scan-modules.md new file mode 100644 index 0000000..3f2c225 --- /dev/null +++ b/soneta-programming/references/scan-modules.md @@ -0,0 +1,137 @@ +# Skanowanie modułów i tabel z DLL (Roslyn MetadataReference) + +Narzędzie do wylistowania wszystkich modułów (`*Module`) platformy enova365/Soneta oraz tabel +(`*Row` / `*Table`) zdefiniowanych w każdym z nich. Czyta metadane skompilowanych bibliotek dodatku, +nie wymaga źródeł. + +## Cel + +W modelu Soneta każda baza danych jest opisana zbiorem modułów (`HandelModule`, `KadryModule`, +`CoreModule`, …), a każdy moduł zawiera zagnieżdżone klasy `*Row` definiujące pojedyncze tabele. +Skrypt pozwala szybko zinwentaryzować całą strukturę: jakie moduły są obecne w bibliotekach, +jakie tabele zawierają i jak nazywa się klasa `*Table` używana w sesji (`Session.Tables.*`). + +Używaj tego narzędzia, gdy: +- eksplorujesz nieznany zestaw bibliotek i chcesz zobaczyć pełną listę modułów/tabel; +- chcesz znaleźć właściwą nazwę `RowType` lub `TableType` przed użyciem skryptu + [scan-props](./scan-props.md); +- przygotowujesz dodatek/raport, który potrzebuje pełnego mapowania klasa biznesowa ↔ nazwa tabeli; +- weryfikujesz, że nowy dodatek został poprawnie zarejestrowany (jego `*Module` pojawia się na liście). + +## Mechanizm + +Skrypt używa **Roslyn** (`Microsoft.CodeAnalysis.CSharp`) i `MetadataReference.CreateFromFile`, +czyli metadane są czytane bez ładowania IL do CLR — bezpiecznie, bez ryzyka konfliktów wersji, +x86/x64 itp. + +Algorytm: +1. Zbierz wszystkie `*.dll` z podanego katalogu i zarejestruj jako `MetadataReference`. Dodatkowo + dołącz biblioteki runtime'u .NET z listy TPA + (`AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")`) — bez tego Roslyn nie rozwiązuje + `CaptionAttribute` / `DescriptionAttribute` i `ConstructorArguments` zwraca pustą tablicę, + przez co `Tytuł`/`Opis` zostają puste. +2. Zbuduj `CSharpCompilation` z tymi referencjami. +3. Przejdź rekurencyjnie po `IAssemblySymbol.GlobalNamespace` każdej referencji. +4. Wybierz wszystkie publiczne klasy top-level o nazwie kończącej się na `Module`, + które dziedziczą z `Soneta.Business.Module` (sprawdzane po `BaseType`). + Filtr eliminuje "śmieci" w stylu `System.Reflection.RuntimeModule`. +5. Dla każdego modułu: + - znajdź zagnieżdżone klasy o nazwie kończącej się na `Row` (`module.GetTypeMembers()`); + - `RowType` = nazwa klasy bez sufiksu `Row` (np. `DokumentHandlowyRow` → `DokumentHandlowy`); + - `TableType` = nazwa typu property `Table` w klasie `*Row` (przeszukiwany wraz z dziedziczeniem + przez `FindMemberInherited`); + - `Tytuł` = `CaptionAttribute`, `Opis` = `DescriptionAttribute` z klasy `*Table` zagnieżdżonej + w tym samym module (np. `HandelModule.DokumentHandlowyTable`). Atrybuty są deklarowane + w l.mn. („Dokumenty handlowe"), bo opisują tabelę. Fallback: jeśli klasy `*Table` brak + lub nie ma atrybutu, czytane są te same atrybuty z klasy `*Row`. Wartością jest pierwszy + parametr `string` konstruktora atrybutu. + - `Guided` = `tak`, gdy klasa `*Table` dziedziczy (bezpośrednio lub pośrednio) z `GuidedTable` + albo `ExportedTable`. Tabele oznaczone `Guided=tak` są **rootami drzewa obiektów** — + stanowią korzeń paczki danych (`Datapack`/`GuidedRow`/`ExportedRow`) i to one są obsługiwane + przez mechanizm synchronizacji i eksportu/importu. Tabele bez tej flagi to elementy + szczegółowe (subrowy, info-rowy), które są częścią paczki danej tabeli-korzenia, ale nie + stanowią samodzielnego rootu. + - Dla samego modułu (`*Module`) Tytuł/Opis czytane są analogicznie z atrybutów na klasie modułu. +6. Wypisz markdown: sekcja `##` per moduł (z jego `Caption`/`Description` jeśli są), w każdej + sekcji tabela `RowType | TableType | Tytuł | Opis`. + +## Wymagania + +- .NET SDK (8.0+) +- `dotnet-script`: + ```bash + dotnet tool install -g dotnet-script + ``` + +## Uruchomienie + +```bash +dotnet script ~/.claude/skills/soneta-programming/scripts/scan-modules.csx \ + -- +``` + +### Przykład + +```bash +dotnet script ~/.claude/skills/soneta-programming/scripts/scan-modules.csx \ + -- ./bin/Debug/net8.0 +``` + +### Przykładowe wyjście + +```markdown +# Moduły i tabele (Soneta) + +Znaleziono modułów: 37 + +## `Soneta.Handel.HandelModule` + +- Opis: Moduł handlowy obsługujący dokumenty sprzedaży, zakupu, zamówień i innych operacji handlowych... +- Tabel: 62 + +| RowType | TableType | Guided | Tytuł | Opis | +|---------|-----------|--------|-------|------| +| DefDokHandlowego | DefDokHandlowych | tak | Definicje dokumentów handlowych | Konfigurowalna definicja (szablon) dokumentu handlowego... | +| DefRelacjiHandlowej | DefRelHandlowych | tak | Definicje relacji handlowych | Konfigurowalna definicja relacji między dokumentami handlowymi... | +| DokumentHandlowy | DokHandlowe | tak | Dokumenty handlowe | Główna tabela dokumentów handlowych (faktury, paragony, zamówienia, korekty, umowy itp.)... | +| DokumentHandlowyKoszt | DokHandloweKoszt | | Koszty dodatkowe | Koszt dodatkowy przypisany do dokumentu handlowego... | +| DrukarkaFiskalna | DrukarkiFiskalne | tak | Lista drukarek fiskalnych | Konfiguracja drukarki fiskalnej... | +| ... | ... | ... | ... | ... | + +_Łącznie tabel: 1196_ +``` + +## Kody wyjścia + +| Kod | Znaczenie | +|-----|-----------| +| `0` | OK — wypisano listę modułów i tabel | +| `1` | Błąd argumentów / nie istnieje katalog / brak DLL | + +## Ograniczenia + +- Skanuje tylko górny poziom katalogu (`SearchOption.TopDirectoryOnly`) — jeśli DLL są + rozproszone, skopiuj je do jednego katalogu. +- `TableType` dla abstrakcyjnych klas `*Row` (subrowy, klasy bazowe) jest często równy `Table` — + to znaczy, że property `Table` pochodzi z klasy bazowej `Soneta.Business.Row` i zwraca ogólny + typ `Soneta.Business.Table`, a klasa `*Row` nie ma własnej, dedykowanej tabeli. +- Pierwsze uruchomienie pobiera pakiet NuGet `Microsoft.CodeAnalysis.CSharp` — wymaga + połączenia internetowego (kolejne odpalenia działają offline). + +## Typowy workflow + +1. **Wstępna inwentaryzacja** — uruchom `scan-modules.csx`, żeby zobaczyć pełną listę + `RowType`/`TableType`. +2. **Drążenie szczegółów** — dla wybranego `RowType` (np. `DokumentHandlowy`) uruchom + [scan-props.csx](./scan-props.md) i odczytaj listę pól bazodanowych oraz właściwości + kalkulowanych klasy biznesowej. +3. **Generowanie kodu / form.xml / warunków** — użyj odczytanych nazw i typów do budowania + wyrażeń bindujących, warunków filtrujących, kodu workerów lub Datapacków. + +## Powiązania + +- [scan-props.md](./scan-props.md) — drugi skrypt skanujący, dla pojedynczego rekordu wypisuje + pełną listę pól (bazodanowych + kalkulowanych) wraz z `Tytuł`/`Opis` i rekurencyjnym + rozwinięciem subrowów. +- Patrz skill `soneta-business-xml` — definicje schematu z których `BusinessGenerator` + produkuje klasy `*Module`, `*Row`, `*Table` i `*Record`. diff --git a/soneta-programming/references/scan-props.md b/soneta-programming/references/scan-props.md new file mode 100644 index 0000000..d424779 --- /dev/null +++ b/soneta-programming/references/scan-props.md @@ -0,0 +1,109 @@ +# Skanowanie pól klasy biznesowych z DLL (Roslyn MetadataReference) + +Narzędzie do odczytu rzeczywistych pól bazodanowych obiektu biznesowego ze skompilowanych bibliotek dodatku +enova365/Soneta. +Pozwala to na budowanie kodu opierającego się na tych klasach biznesowych, wyrażeń bindujących form.xml oraz +(Warunki filtrujące)[./rowconditions.md]. + +## Cel + +W modelu Soneta klasa `Row` (np. `DokumentHandlowy`) udostępnia właściwości publiczne, które można wykorzystać w +generowanych kodzie biznesowym, oraz do budowania wyrażeń bindujących form.xml. Natomiast w +(warunkach filtrujących)[./rowconditions.md], można używać TYLKO pól bazodananowych udostępnianych przez to narzędzie. + +Używaj tego narzędzia, gdy: +- piszesz kod operujący bezpośrednio na polach rekordu (np. w extenderze, workerze, datapack); +- chcesz zweryfikować rzeczywisty typ pola (np. `decimal?` vs `decimal`) bez czytania wygenerowanego kodu; +- pracujesz na dodatku innej osoby i nie masz dostępu do źródeł, tylko do DLL. +- generujesz warunki filtrujące (serwerowe - LINQ) +- Przygotowujesz formularze form.xml. + +## Mechanizm + +Skrypt używa **Roslyn** (`Microsoft.CodeAnalysis.CSharp`) i `MetadataReference.CreateFromFile`, co oznacza, że **metadane są czytane bez ładowania IL** do CLR — bezpiecznie, bez ryzyka konfliktów wersji, x86/x64 itp. + +Algorytm: +1. Zbierz wszystkie `*.dll` z podanego katalogu i zarejestruj jako `MetadataReference`. Dodatkowo dołącz wszystkie biblioteki runtime'u .NET z listy TPA (`AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")`) — bez tego Roslyn nie rozwiązuje typu `System.ComponentModel.DescriptionAttribute` (i podobnych) i `ConstructorArguments` atrybutów zwraca pustą tablicę, przez co `Tytuł`/`Opis` zostają puste. +2. Zbuduj `CSharpCompilation` z tymi referencjami. +3. Przejdź rekurencyjnie po `IAssemblySymbol.GlobalNamespace` każdej referencji. +4. Znajdź pierwszy typ kończący się na `Module`, który zawiera typ zagnieżdżony o nazwie `{NazwaRekordu}Record`. +5. Odczytaj publiczne pola (`IFieldSymbol`, `DeclaredAccessibility == Public`) i ich typy → oznacz jako **bazodanowe**. +6. Znajdź publiczną klasę najwyższego poziomu o nazwie `{NazwaRekordu}` (klasę biznesową, np. `DokumentHandlowy`) i wczytaj jej publiczne, instancyjne `IPropertySymbol` (wraz z dziedziczonymi). +7. Scal listy: + - property o nazwie unikalnej (brak takiego pola w rekordzie) → oznacz jako **kalkulowane**; + - property o nazwie pokrywającej się z polem rekordu → zachowaj znacznik **bazodanowe**, ale podmień typ na ten z property (bo property zwykle precyzuje typ, np. zwraca konkretny enum lub `Row` zamiast `Guid`/`int`). +8. Dla każdego wpisu odczytaj `Tytuł` i `Opis` — pierwszy parametr `string` konstruktora atrybutu. Matching nazwy atrybutu jest dopasowywany do `Caption`/`CaptionAttribute` oraz `Description`/`DescriptionAttribute` (np. `System.ComponentModel.DescriptionAttribute` używany w generowanym kodzie Soneta). Kolejność źródeł: + 1. property klasy biznesowej (`{NazwaRekordu}`) — z uwzględnieniem dziedziczenia (atrybut może być na property bazowej klasy, np. `{NazwaRekordu}Row`); + 2. pole rekordu (`{NazwaModulu}+{NazwaRekordu}Record`); + 3. odpowiadający member w typie zagnieżdżonym `{NazwaModulu}+{NazwaRekordu}Row` (fallback — przeszukiwany wraz z klasami bazowymi). To tam Soneta zwykle deklaruje `[Description("...")]` na publicznych property delegujących do pola rekordu. +9. **Rekurencja po subrowach** — jeśli któreś z pól rekordu ma typ kończący się na `Record` (np. `CoreModule.DefinicjaNumeracjiRecord Numeracja`), traktuj je jako subrow: + - na bazie nazwy typu wylicz nazwę bazową (`DefinicjaNumeracjiRecord` → `DefinicjaNumeracji`); + - znajdź klasę biznesową (`DefinicjaNumeracji`) oraz typ `*Module+DefinicjaNumeracjiRow` (mogą być w innym module — np. `CoreModule`); + - powtórz całą procedurę (kroki 5–8) dla tego rekordu, używając prefiksu `Numeracja.` w kluczach wyników (`Numeracja.Pole1`, `Numeracja.Pole2`, …). + Rekurencja działa dowolnie głęboko (subrow w subrowie). Pętle (rekord zawierający siebie pośrednio) są zabezpieczone przez zbiór odwiedzonych typów. +10. Wypisz tabelę markdown na stdout (kolumny: `Pole | Typ | Rodzaj | Tytuł | Opis`). + +## Wymagania + +- .NET SDK (8.0+) +- `dotnet-script`: + ```bash + dotnet tool install -g dotnet-script + ``` + +## Uruchomienie + +```bash +dotnet script ~/.claude/skills/soneta-programming/scripts/scan-props.csx \ + -- +``` + +### Przykład + +```bash +dotnet script ~/.claude/skills/soneta-programming/scripts/scan-props.csx \ + -- DokumentHandlowy ./bin/Debug/net8.0 +``` + +### Przykładowe wyjście + +```markdown +# Pola i właściwości klasy biznesowej: `Soneta.Handel.DokumentHandlowy` +Nazwa tabeli: `DokHandlowe` + +- pola bazodanowe: 128 +- pola kalkulowane (z klas biznesowych): 388 + +| Pole | Typ | Rodzaj | Tytuł | Opis | +|------|-----|--------|-------|------| +| Brutto | `decimal` | bazodanowe | Brutto | Wartość brutto dokumentu | +| DataDokumentu | `System.DateTime` | bazodanowe | Data dokumentu | | +| Kontrahent | `Soneta.Kontrahenci.Kontrahent` | bazodanowe | Kontrahent | | +| Netto | `decimal` | bazodanowe | Netto | | +| Numer | `string` | bazodanowe | Numer | | +| SaldoWaluta | `decimal` | | Saldo w walucie | | +| ... | ... | ... | ... | ... | +``` + +Kolumna `Rodzaj` ma wartość `bazodanowe` dla pól rekordu lub jest pusta dla właściwości kalkulowanych. + +## Kody wyjścia + +| Kod | Znaczenie | +|-----|-----------| +| `0` | OK — wypisano tabelę pól | +| `1` | Błąd argumentów / nie istnieje katalog / brak DLL | +| `2` | Nie znaleziono typu `*Module+{NazwaRekordu}Record` w referencjach | + +## Ograniczenia + +- Skanuje tylko górny poziom katalogu (`SearchOption.TopDirectoryOnly`) — jeśli DLL są rozproszone, skopiuj je do jednego katalogu. +- Zwraca pierwszy znaleziony typ pasujący do wzorca `*Module+{Nazwa}Record` — jeśli dwa moduły mają taki sam zagnieżdżony rekord, dostaniesz tylko jeden (niedeterministycznie wg kolejności assembly). +- Zwraca **publiczne pola** rekordu (`IFieldSymbol`) oraz **publiczne, instancyjne właściwości** klasy biznesowej (`IPropertySymbol`, łącznie z dziedziczonymi). Pola rekordu = źródło prawdy o schemacie DB (rodzaj `bazodanowe`); właściwości spoza rekordu = wyliczane w kodzie (rodzaj `kalkulowane`). +- Jeśli klasa biznesowa o nazwie `{NazwaRekordu}` nie zostanie znaleziona w referencjach, skrypt zwraca tylko listę pól bazodanowych (z odpowiednią adnotacją w nagłówku) i kończy się kodem `0`. +- Pierwsze uruchomienie pobiera pakiet NuGet `Microsoft.CodeAnalysis.CSharp` — wymaga połączenia internetowego (kolejne odpalenia działają offline). + +## Powiązania + +- Patrz [datapack-guidedrow.md](datapack-guidedrow.md) — struktury `GuidedRow` / `ExportedRow` i mechanizm Datapack operujący na polach rekordu. +- Patrz skill `soneta-business-xml` — definicja schematu, z którego `BusinessGenerator` produkuje klasę `XxxRecord`. diff --git a/soneta-programming/scripts/scan-modules.csx b/soneta-programming/scripts/scan-modules.csx new file mode 100644 index 0000000..5c1cbc6 --- /dev/null +++ b/soneta-programming/scripts/scan-modules.csx @@ -0,0 +1,215 @@ +#r "nuget: Microsoft.CodeAnalysis.CSharp, 4.11.0" + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +if (Args.Count < 1) +{ + Console.Error.WriteLine("Użycie: dotnet script scan-modules.csx -- "); + Console.Error.WriteLine("Przykład: dotnet script scan-modules.csx -- ./bin/Debug/net8.0"); + return 1; +} + +var dllDir = Path.GetFullPath(Args[0]); +if (!Directory.Exists(dllDir)) +{ + Console.Error.WriteLine($"Katalog nie istnieje: {dllDir}"); + return 1; +} + +var dllPaths = Directory.EnumerateFiles(dllDir, "*.dll", SearchOption.TopDirectoryOnly).ToList(); +if (dllPaths.Count == 0) +{ + Console.Error.WriteLine($"Brak plików *.dll w katalogu: {dllDir}"); + return 1; +} + +var refs = new List(); +var addedPaths = new HashSet(StringComparer.OrdinalIgnoreCase); +foreach (var p in dllPaths) +{ + try + { + refs.Add(MetadataReference.CreateFromFile(p)); + addedPaths.Add(Path.GetFileName(p)); + } + catch (Exception ex) { Console.Error.WriteLine($"# Pominięto {Path.GetFileName(p)}: {ex.Message}"); } +} + +var tpa = (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") as string ?? "") + .Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries); +foreach (var path in tpa) +{ + var name = Path.GetFileName(path); + if (addedPaths.Contains(name)) continue; + try { refs.Add(MetadataReference.CreateFromFile(path)); addedPaths.Add(name); } + catch { /* pomiń */ } +} + +var compilation = CSharpCompilation.Create("ScanModules") + .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) + .AddReferences(refs); + +var modules = new List(); +foreach (var asmRef in compilation.References) +{ + if (compilation.GetAssemblyOrModuleSymbol(asmRef) is not IAssemblySymbol asm) continue; + foreach (var type in EnumerateAllTypes(asm.GlobalNamespace)) + { + if (type.ContainingType != null) continue; + if (type.TypeKind != TypeKind.Class) continue; + if (!type.Name.EndsWith("Module")) continue; + if (!InheritsFromModule(type)) continue; + modules.Add(type); + } +} + +modules = modules.OrderBy(m => m.ToDisplayString(), StringComparer.Ordinal).ToList(); + +Console.WriteLine("# Moduły i tabele (Soneta)"); +Console.WriteLine(); +Console.WriteLine($"Znaleziono modułów: {modules.Count}"); +Console.WriteLine(); + +var totalRows = 0; +foreach (var module in modules) +{ + // Indeks zagnieżdżonych typów *Table — Row bez własnej klasy *Table nie jest realną tabelą + // (jest subrow/abstrakcyjną klasą bazową), więc filtrujemy go z wyników. + var tableClasses = module.GetTypeMembers() + .Where(t => t.TypeKind == TypeKind.Class && t.Name.EndsWith("Table")) + .ToDictionary(t => t.Name, StringComparer.Ordinal); + + var rowClasses = module.GetTypeMembers() + .Where(t => t.TypeKind == TypeKind.Class && t.Name.EndsWith("Row")) + .Where(t => tableClasses.ContainsKey( + t.Name.Substring(0, t.Name.Length - "Row".Length) + "Table")) + .OrderBy(t => t.Name, StringComparer.Ordinal) + .ToList(); + + var moduleShortName = module.Name.EndsWith("Module") + ? module.Name.Substring(0, module.Name.Length - "Module".Length) + : module.Name; + Console.WriteLine($"## {moduleShortName}"); + Console.WriteLine(); + Console.WriteLine($"- Klasa: `{module.ToDisplayString()}`"); + Console.WriteLine(); + var modCaption = GetAttributeFirstString(module, "CaptionAttribute"); + var modDescription = GetAttributeFirstString(module, "DescriptionAttribute"); + if (!string.IsNullOrEmpty(modCaption)) Console.WriteLine($"- Tytuł: {modCaption}"); + if (!string.IsNullOrEmpty(modDescription)) Console.WriteLine($"- Opis: {modDescription}"); + Console.WriteLine($"- Tabel: {rowClasses.Count}"); + Console.WriteLine(); + + if (rowClasses.Count == 0) + { + Console.WriteLine("_Brak klas `*Row` w tym module._"); + Console.WriteLine(); + continue; + } + + Console.WriteLine("| RowType | TableType | Guided | Tytuł | Opis |"); + Console.WriteLine("|---------|-----------|--------|-------|------|"); + foreach (var row in rowClasses) + { + var rowType = row.Name.EndsWith("Row") + ? row.Name.Substring(0, row.Name.Length - "Row".Length) + : row.Name; + + var tableProp = FindMemberInherited(row, "Table") as IPropertySymbol; + var tableType = tableProp?.Type is INamedTypeSymbol nt + ? nt.Name + : (tableProp?.Type?.ToDisplayString() ?? ""); + + // Atrybuty Caption/Description siedzą na klasie *Table zagnieżdżonej w module + // (np. `HandelModule.DokumentHandlowyTable`). Klasa *Row jest fallbackiem na wypadek, + // gdyby konwencja w przyszłości się zmieniła. + tableClasses.TryGetValue(rowType + "Table", out var tableCls); + var caption = GetAttributeFirstString(tableCls, "CaptionAttribute"); + if (string.IsNullOrEmpty(caption)) + caption = GetAttributeFirstString(row, "CaptionAttribute"); + var description = GetAttributeFirstString(tableCls, "DescriptionAttribute"); + if (string.IsNullOrEmpty(description)) + description = GetAttributeFirstString(row, "DescriptionAttribute"); + + var guided = tableCls != null && InheritsFromGuidedOrExportedTable(tableCls) + ? "tak" + : ""; + + Console.WriteLine($"| {rowType} | {tableType} | {guided} | {EscapeCell(caption)} | {EscapeCell(description)} |"); + totalRows++; + } + Console.WriteLine(); +} + +Console.WriteLine($"_Łącznie tabel: {totalRows}_"); +return 0; + +static IEnumerable EnumerateAllTypes(INamespaceSymbol ns) +{ + foreach (var t in ns.GetTypeMembers()) yield return t; + foreach (var sub in ns.GetNamespaceMembers()) + foreach (var t in EnumerateAllTypes(sub)) yield return t; +} + +static bool InheritsFromGuidedOrExportedTable(INamedTypeSymbol type) +{ + for (var t = type.BaseType; t != null && t.SpecialType != SpecialType.System_Object; t = t.BaseType) + { + if (t.Name == "GuidedTable" || t.Name == "ExportedTable") return true; + } + return false; +} + +static bool InheritsFromModule(INamedTypeSymbol type) +{ + for (var t = type.BaseType; t != null && t.SpecialType != SpecialType.System_Object; t = t.BaseType) + { + if (t.Name == "Module" && t.ContainingNamespace?.ToDisplayString() == "Soneta.Business") + return true; + } + return false; +} + +static ISymbol FindMemberInherited(INamedTypeSymbol type, string name) +{ + for (var t = type; t != null && t.SpecialType != SpecialType.System_Object; t = t.BaseType) + { + var m = t.GetMembers(name).FirstOrDefault(); + if (m != null) return m; + } + return null; +} + +static string GetAttributeFirstString(ISymbol symbol, string attributeTypeName) +{ + if (symbol == null) return ""; + var shortName = attributeTypeName.EndsWith("Attribute") + ? attributeTypeName.Substring(0, attributeTypeName.Length - "Attribute".Length) + : attributeTypeName; + var longName = shortName + "Attribute"; + foreach (var a in symbol.GetAttributes()) + { + if (a.AttributeClass == null) continue; + var n = a.AttributeClass.Name; + if (!string.Equals(n, shortName, StringComparison.Ordinal) + && !string.Equals(n, longName, StringComparison.Ordinal)) + continue; + foreach (var arg in a.ConstructorArguments) + { + if (arg.Kind == TypedConstantKind.Primitive && arg.Value is string s) + return s; + } + } + return ""; +} + +static string EscapeCell(string s) +{ + if (string.IsNullOrEmpty(s)) return ""; + return s.Replace("\\", "\\\\").Replace("|", "\\|").Replace("\r", " ").Replace("\n", " "); +} diff --git a/soneta-programming/scripts/scan-props.csx b/soneta-programming/scripts/scan-props.csx new file mode 100644 index 0000000..9efe41d --- /dev/null +++ b/soneta-programming/scripts/scan-props.csx @@ -0,0 +1,307 @@ +#r "nuget: Microsoft.CodeAnalysis.CSharp, 4.11.0" + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +if (Args.Count < 2) +{ + Console.Error.WriteLine("Użycie: dotnet script scan-props.csx -- "); + Console.Error.WriteLine("Przykład: dotnet script scan-props.csx -- DokumentHandlowy ./bin/Debug/net8.0"); + return 1; +} + +var recordBaseName = Args[0]; +var dllDir = Path.GetFullPath(Args[1]); +var nestedTypeName = recordBaseName + "Record"; + +if (!Directory.Exists(dllDir)) +{ + Console.Error.WriteLine($"Katalog nie istnieje: {dllDir}"); + return 1; +} + +var dllPaths = Directory.EnumerateFiles(dllDir, "*.dll", SearchOption.TopDirectoryOnly).ToList(); +if (dllPaths.Count == 0) +{ + Console.Error.WriteLine($"Brak plików *.dll w katalogu: {dllDir}"); + return 1; +} + +var refs = new List(); +var addedPaths = new HashSet(StringComparer.OrdinalIgnoreCase); +foreach (var p in dllPaths) +{ + try + { + refs.Add(MetadataReference.CreateFromFile(p)); + addedPaths.Add(Path.GetFileName(p)); + } + catch (Exception ex) { Console.Error.WriteLine($"# Pominięto {Path.GetFileName(p)}: {ex.Message}"); } +} + +// Dodaj referencje do bibliotek runtime'u .NET (TPA — Trusted Platform Assemblies), +// żeby Roslyn potrafił rozwiązać atrybuty typu System.ComponentModel.DescriptionAttribute +// i zdekodować ich argumenty konstruktora. +var tpa = (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") as string ?? "") + .Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries); +foreach (var path in tpa) +{ + var name = Path.GetFileName(path); + if (addedPaths.Contains(name)) continue; + try + { + refs.Add(MetadataReference.CreateFromFile(path)); + addedPaths.Add(name); + } + catch { /* pomiń */ } +} + +var compilation = CSharpCompilation.Create("Scan") + .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) + .AddReferences(refs); + +INamedTypeSymbol foundRecord = null; +INamedTypeSymbol enclosing = null; + +// Indeks publicznych klas najwyższego poziomu po nazwie (do wyszukiwania +// klas biznesowych — głównej oraz dla subrowów). +var topLevelClasses = new Dictionary(StringComparer.Ordinal); + +foreach (var asmRef in compilation.References) +{ + if (compilation.GetAssemblyOrModuleSymbol(asmRef) is not IAssemblySymbol asm) continue; + foreach (var type in EnumerateAllTypes(asm.GlobalNamespace)) + { + if (foundRecord == null && type.Name.EndsWith("Module")) + { + var nested = type.GetTypeMembers(nestedTypeName).FirstOrDefault(); + if (nested != null) + { + foundRecord = nested; + enclosing = type; + } + } + if (type.ContainingType == null + && type.TypeKind == TypeKind.Class + && type.DeclaredAccessibility == Accessibility.Public) + { + topLevelClasses.TryAdd(type.Name, type); + } + } +} + +if (foundRecord == null) +{ + Console.Error.WriteLine($"Nie znaleziono typu *Module+{nestedTypeName} w {dllDir}"); + return 2; +} + +INamedTypeSymbol mainBusinessClass = null; +topLevelClasses.TryGetValue(recordBaseName, out mainBusinessClass); + +// Nazwa tabeli wyciągana z typu zwracanego przez property `Table` w klasie XxxxRow. +string tableTypeName = null; +var rowClass = enclosing?.GetTypeMembers(recordBaseName + "Row").FirstOrDefault(); +if (rowClass != null) +{ + for (var t = rowClass; t != null && t.SpecialType != SpecialType.System_Object; t = t.BaseType) + { + var tableProp = t.GetMembers("Table").OfType().FirstOrDefault(); + if (tableProp != null) + { + tableTypeName = tableProp.Type.Name; + break; + } + } +} + +// Klucz: nazwa pola z notacją kropkową dla subrowów; Wartość: (typ, czyBazodanowe, tytuł, opis) +var merged = new SortedDictionary(StringComparer.Ordinal); + +var visited = new HashSet(SymbolEqualityComparer.Default); +ScanRecord(foundRecord, "", visited, merged, topLevelClasses); + +if (mainBusinessClass != null) +{ + Console.WriteLine($"# Pola i właściwości klasy biznesowej: `{mainBusinessClass.ToDisplayString()}`"); +} +else +{ + Console.WriteLine($"# Pola i właściwości `{enclosing.ToDisplayString()}+{nestedTypeName}`"); + Console.WriteLine(); + Console.WriteLine($"Nie znaleziono klasy biznesowej `{recordBaseName}` — pokazano tylko pola bazodanowe."); +} +if (!string.IsNullOrEmpty(tableTypeName)) +{ + Console.WriteLine($"Nazwa tabeli: `{tableTypeName}`"); +} +Console.WriteLine(); +var dbCount = merged.Values.Count(v => v.IsDb); +var calcCount = merged.Count - dbCount; +Console.WriteLine($"- pola bazodanowe: {dbCount}"); +Console.WriteLine($"- pola kalkulowane (z klas biznesowych): {calcCount}"); +Console.WriteLine(); +Console.WriteLine("| Pole | Typ | Rodzaj | Tytuł | Opis |"); +Console.WriteLine("|------|-----|--------|-------|------|"); +foreach (var kv in merged) +{ + var rodzaj = kv.Value.IsDb ? "bazodanowe" : ""; + Console.WriteLine($"| {kv.Key} | `{kv.Value.Type}` | {rodzaj} | {EscapeCell(kv.Value.Caption)} | {EscapeCell(kv.Value.Description)} |"); +} +return 0; + +static void ScanRecord( + INamedTypeSymbol record, + string prefix, + HashSet visited, + SortedDictionary merged, + Dictionary topLevelClasses) +{ + if (record == null) return; + if (!visited.Add(record)) return; + + var fields = record.GetMembers() + .OfType() + .Where(f => f.DeclaredAccessibility == Accessibility.Public) + .ToList(); + + var encMod = record.ContainingType; + var baseName = record.Name.EndsWith("Record") + ? record.Name.Substring(0, record.Name.Length - "Record".Length) + : record.Name; + + INamedTypeSymbol bizCls = null; + topLevelClasses.TryGetValue(baseName, out bizCls); + var rowFallback = encMod?.GetTypeMembers(baseName + "Row").FirstOrDefault(); + + // 1. Pola rekordu → bazodanowe. + foreach (var f in fields) + { + var key = prefix + f.Name; + merged[key] = ( + f.Type.ToDisplayString(), + true, + GetAttributeFirstString(f, "CaptionAttribute"), + GetAttributeFirstString(f, "DescriptionAttribute")); + } + + // 2. Właściwości klasy biznesowej (z dziedziczeniem) → kalkulowane lub nadpisanie pola bazodanowego. + if (bizCls != null) + { + var seen = new HashSet(StringComparer.Ordinal); + foreach (var p in EnumerateInheritedProperties(bizCls)) + { + if (p.DeclaredAccessibility != Accessibility.Public || p.IsStatic || p.IsIndexer || p.GetMethod == null) + continue; + if (!seen.Add(p.Name)) continue; + var key = prefix + p.Name; + var typeStr = p.Type.ToDisplayString(); + var caption = GetAttributeFirstString(p, "CaptionAttribute"); + var description = GetAttributeFirstString(p, "DescriptionAttribute"); + if (merged.TryGetValue(key, out var existing)) + { + merged[key] = ( + typeStr, + existing.IsDb, + !string.IsNullOrEmpty(caption) ? caption : existing.Caption, + !string.IsNullOrEmpty(description) ? description : existing.Description); + } + else + { + merged[key] = (typeStr, false, caption, description); + } + } + } + + // 3. Fallback — atrybuty Caption/Description z typu zagnieżdżonego *Row (wraz z dziedziczeniem). + if (rowFallback != null) + { + var prefixLen = prefix.Length; + foreach (var key in merged.Keys.ToList()) + { + if (!key.StartsWith(prefix, StringComparison.Ordinal)) continue; + var local = key.Substring(prefixLen); + if (local.Length == 0 || local.Contains('.')) continue; + var entry = merged[key]; + if (!string.IsNullOrEmpty(entry.Caption) && !string.IsNullOrEmpty(entry.Description)) + continue; + var member = FindMemberInherited(rowFallback, local); + if (member == null) continue; + var caption = !string.IsNullOrEmpty(entry.Caption) + ? entry.Caption + : GetAttributeFirstString(member, "CaptionAttribute"); + var description = !string.IsNullOrEmpty(entry.Description) + ? entry.Description + : GetAttributeFirstString(member, "DescriptionAttribute"); + merged[key] = (entry.Type, entry.IsDb, caption, description); + } + } + + // 4. Rekurencja po polach typu subrow (typ kończący się na "Record"). + foreach (var f in fields) + { + if (f.Type is INamedTypeSymbol nested && nested.TypeKind == TypeKind.Class && nested.Name.EndsWith("Record")) + { + ScanRecord(nested, prefix + f.Name + ".", visited, merged, topLevelClasses); + } + } +} + +static IEnumerable EnumerateAllTypes(INamespaceSymbol ns) +{ + foreach (var t in ns.GetTypeMembers()) yield return t; + foreach (var sub in ns.GetNamespaceMembers()) + foreach (var t in EnumerateAllTypes(sub)) yield return t; +} + +static IEnumerable EnumerateInheritedProperties(INamedTypeSymbol type) +{ + for (var t = type; t != null && t.SpecialType != SpecialType.System_Object; t = t.BaseType) + { + foreach (var p in t.GetMembers().OfType()) + yield return p; + } +} + +static string GetAttributeFirstString(ISymbol symbol, string attributeTypeName) +{ + if (symbol == null) return ""; + var shortName = attributeTypeName.EndsWith("Attribute") + ? attributeTypeName.Substring(0, attributeTypeName.Length - "Attribute".Length) + : attributeTypeName; + var longName = shortName + "Attribute"; + foreach (var a in symbol.GetAttributes()) + { + if (a.AttributeClass == null) continue; + var n = a.AttributeClass.Name; + if (!string.Equals(n, shortName, StringComparison.Ordinal) + && !string.Equals(n, longName, StringComparison.Ordinal)) + continue; + foreach (var arg in a.ConstructorArguments) + { + if (arg.Kind == TypedConstantKind.Primitive && arg.Value is string s) + return s; + } + } + return ""; +} + +static ISymbol FindMemberInherited(INamedTypeSymbol type, string name) +{ + for (var t = type; t != null && t.SpecialType != SpecialType.System_Object; t = t.BaseType) + { + var m = t.GetMembers(name).FirstOrDefault(); + if (m != null) return m; + } + return null; +} + +static string EscapeCell(string s) +{ + if (string.IsNullOrEmpty(s)) return ""; + return s.Replace("\\", "\\\\").Replace("|", "\\|").Replace("\r", " ").Replace("\n", " "); +}