odd-one-out
v0.4.0
Published
Finds places that deviate from the convention of the project itself: N times this way, once differently. Java, JavaScript, TypeScript, Maven pom.xml, SQL migrations on Supabase.
Maintainers
Readme
odd-one-out
Dla każdego, kto pracuje w kodzie, którego reguł nikt nie spisał — własnym sprzed dwóch lat albo cudzym sprzed tygodnia. Nawyki siedzą w kodzie, tyle że nie ma ich w żadnym dokumencie i nic nie mówi, które z nich były zamierzone.
odd-one-out wyczytuje te nawyki z repozytorium i pokazuje miejsca, które je łamią: to jedno wywołanie z pięćdziesięciu, które zapomniało zwolnić odtwarzacz, tę jedną migrację, która odbiera uprawnienia i nigdy ich nie oddaje. Zasada jest jedna: N razy tak, raz inaczej.
Klasa z tysiącem linii w projekcie, gdzie wszystkie mają tysiąc, nie odstaje.
Cztery MediaPlayer z setOnError i jeden bez — odstaje. Narzędzie nie ma
progów z sufitu; porównuje kod do reszty tego samego repozytorium.
Nie zmienia plików. Przy każdym zgłoszeniu pokazuje gotową poprawkę do wklejenia.
Co się zmieniło w 0.4.0
Jeśli uruchamiasz deps w CI, przeczytaj ten wiersz: teraz może zwrócić
1. Każdy inny detektor czyta poprzedni przebieg i zgłasza to, co NOWE.
deps zapisywał migawki i nigdy żadnej nie czytał, więc drugi przebieg
wypisywał te same odchylenia co pierwszy, co do słowa — i notka z 0.3.0 niżej
mówiła dokładnie to, przepraszając za to na ekranie. Teraz idzie tą samą drogą
co reszta: 1, gdy odchylenie pojawiło się od ostatniego razu, 0 dla stanu,
który ktoś już widział, --fail-on-state dla chcących drugiego kontraktu.
Zmierzone na 117 klasach prawdziwego drzewa Javy: pierwszy przebieg 18 zgłoszeń
i kod 1; drugi NOWE=0 ZNIKNELO=0 ZMIENIONE=0 bez zmian=18, zero sekcji,
kod 0; --all przywraca pełną listę.
Migawka deps nie jest już cięta przez --top. Powstawała z pierwszych
dziesięciu zgłoszeń, więc baza porównań — i każda różnica wobec niej —
zależała od przełącznika wyświetlania: dwa przebiegi różniące się wyłącznie
--top zgłaszały inne „nowe odchylenia" nad nietkniętym kodem. Plik trzyma
teraz wszystko, a --top ogranicza wydruk, czyli to, do czego służy.
Reszta to bramki, nie funkcje:
- Nowa warstwa uruchamia każdy detektor dwa razy i wymaga, żeby drugi przebieg wiedział o pierwszym. Nic tego wcześniej nie mierzyło: zestaw golden porównuje ZAWARTOŚĆ migawki, a żadna warstwa nie uruchamiała detektora dwukrotnie. Pięć detektorów obiecywało kontrakt różnicowy, cztery go dotrzymywały — przez całe wydanie.
- Bramka README pyta teraz, czy ścieżka z przykładu jest tym, co czytelnik
ma. Każde polecenie z tej strony jest sprawdzane w repozytorium, gdzie
test/istnieje; paczka wieziebin,src,skillsi te dwie strony../src/main/javanazywa katalog w cudzym projekcie i nie jest złamaną obietnicą; ścieżka, która leży tutaj i nie jedzie — jest.
Co się zmieniło w 0.3.0
Jeśli uruchamiasz to w CI, przeczytaj ten wiersz: kody wyjścia się zmieniły.
2 znaczy teraz nic nie było do decyzji, a czegoś nie dało się odczytać —
pusty katalog, plik nie do sparsowania. Wszystkie pięć detektorów wychodziło
tam z 0: każdy wypisywał staranne zdanie, że nie ma nic swojego rodzaju do
przeczytania, i mówił budowaniu, że projekt jest w porządku. Jeśli Twoje
zadanie traktuje każdy niezerowy kod jako porażkę, nic się nie zmienia. Jeśli
je rozróżnia, 2 jest teraz osiągalne tam, gdzie wcześniej było 0.
A deps w ogóle nie umiał zepsuć budowania. Kończył bez ustawienia kodu,
więc zwracał 0 niezależnie od tego, co znalazł — dwadzieścia jeden zgłoszeń
na mierzonym materiale i zielone budowanie nad każdym z nich. Teraz zwraca
2, gdy nic nie dało się odczytać, i 1 przy --fail-on-state. Nadal nie
odróżni NOWEGO zgłoszenia od starego, bo zapisuje migawki i nigdy żadnej nie
czyta — i teraz mówi to na ekranie, zamiast zostawiać to domysłom.
Plik, którego nie da się sparsować, nie unieważnia przebiegu. Drzewa Javy,
na których to mierzono, mają dwa i trzy błędy parsowania i dalej wychodzą z
1, bo zgłosiły sześć i trzysta dziewięćdziesiąt trzy prawdziwe odchylenia.
2 jest dla przebiegu, który nie rozstrzygnął niczego.
Każdy przebieg niesie teraz pole summary, na ekranie i w JSON-ie:
"summary": {
"actionable": 6, "explained": 0, "notApplicable": 0, "unreachable": 2,
"unreachableIs": { "aQuestionForAPerson": 0, "couldNotBeRead": 2 }
}Pięć detektorów nie ma wszystkich czterech stanów. sql ma, bo już wcześniej
liczył powody, dla których coś odkłada; java, js, pom i deps nie mają
kategorii „obejrzane i odłożone" w ogóle, więc te liczby są zerami — bo
kategoria nie istnieje, a nie dlatego, że nic w nią nie wpadło.
Uruchomienie bez instalowania
npx odd-one-out java ./src/main/java
npx odd-one-out js .Node 18 albo nowszy. Nie trzeba klonować ani niczego ustawiać. Pięć detektorów —
java, deps, pom, sql, js — i każdy bierze katalog do przeczytania.
English version of this document: README.md.
Jak wygląda jedno zgłoszenie
Każde zgłoszenie odpowiada na trzy pytania, a trzecie jest powodem, dla którego to narzędzie istnieje: podaje poprawkę i jej nie stosuje.
## [1] Button#setOnMouseEntered -> Button#setStyle sup=45/49 conf=92% odd=4
CO JEST NIESPOJNE
setStyle nie jest tu wolane, choc 45 z 49 miejsc wolajacych
setOnMouseEntered na tym samym odbiorniku je wola. Te 4 nie:
SharedButtonEffects.java:52 recv=btn in setupHoverEffects@38
wolania tutaj: Button#setOnMouseEntered, Button#setOnMouseExited
JAK ZROBIONO W POZOSTALYCH MIEJSCACH
KeyMomentCategoryStyles.java:214 recv=targetButton in applyPreviewToButton@211
wola oba: setOnMouseEntered oraz setStyle
GOTOWA POPRAWKA (nie zastosowana)
// SharedButtonEffects.java:52 — w setupHoverEffects@38, obok wolania, ktore juz tam jest:
+ btn.setStyle(...);
// sprawdz argumenty wzgledem 45 miejsc, ktore je wolaja — regula wie,
// ze wolania brakuje, nie wie, co mu podacCO JEST NIESPÓJNE nazywa populację, na której stoi werdykt — 45 z 49 — więc błędne zgłoszenie da się odrzucić w kilka sekund zamiast je badać. JAK ZROBIONO W POZOSTAŁYCH MIEJSCACH wskazuje prawdziwe linie trzymające wzorzec, bo „to odstaje" jest nic niewarte bez czegoś, do czego można porównać. GOTOWA POPRAWKA to wywołanie do wklejenia, z powiedzianą wprost jedyną rzeczą, której reguła nie wie: wie, którego wywołania brakuje i na czym, nie wie, co mu podać. Nic nigdy nie trafia do pliku — narzędzie, które samo poprawia kod, wymaga testów mierzących, czy pomogło, a takich nie ma.
Instalacja
git clone <repo> && cd odd-one-out
npm install
npm start # wypisuje pomocTrzy sposoby uruchomienia, równoważne:
npx odd-one-out java ./src/main/java # w sklonowanym repo, bez instalowania
node bin/odd-one-out.mjs java ./src/main/java # to samo, bez npx
npm i -g . && odd-one-out java ./src/main/java # instaluje komendę w systemieGołe odd-one-out istnieje dopiero po tym ostatnim. package.json deklaruje
bin, ale zwykły klon niczego nie dokłada do PATH — dlatego ekran pomocy
drukuje npx odd-one-out ....
Dla Claude Code instalacja przez rynek wtyczek (.claude-plugin/marketplace.json
— claude plugin marketplace add <repo>, potem claude plugin install odd-one-out).
Dla pozostałych agentów skopiuj katalog skills/: SKILL.md nie jest formatem
wyłącznie Claude Code — Cursor, Codex i Gemini CLI czytają te same katalogi.
Silnik jest zwykłą paczką npm i działa bez żadnego z nich.
Uwaga o nazwach. Pomiary poniżej pochodzą z prywatnych repozytoriów autora. Nazwy projektów i plików z nich wzięte są zastąpione neutralnymi (
Screen.java,app.html); numery linii, liczby, pozycje i werdykty prawda/fałsz są te zmierzone, bez zmian. Nazwy, które i tak są ogólne (Loading.java,Menu.java), zostały jak były.
Użycie
odd-one-out java <katalog-zrodel-java> # pary wywołań na tym samym odbiorniku
odd-one-out deps <katalog-zrodel-java> # warstwa wspólna vs użycie bezpośrednie
odd-one-out sql <katalog-migracji> # revoke bez grant execute
odd-one-out pom --pom <pom.xml> --tree <deptree.txt>pom wymaga wcześniejszego mvn -o -B dependency:tree > deptree.txt.
Język komunikatów
--lang en (domyślnie) albo --lang pl. Wszystkie napisy siedzą w jednym pliku
src/lang.mjs — klucz i dwa tłumaczenia; detektor woła t('klucz', …) i nie wie,
w jakim języku pisze. Rozsypanie tłumaczeń po detektorach kończy się tym, że
połowa wyniku zostaje w jednym języku, a widzi to dopiero ktoś, kto tego języka
nie zna.
Tłumaczone są tylko etykiety i nagłówki. Nazwy metod, typów, ścieżki, identyfikatory zgłoszeń i nazwy reguł zostają bez zmian — to dane, nie tekst. Zweryfikowane: ten sam zapis wyświetlony w obu językach daje identyczne pozycje, oceny i ścieżki; różni się wyłącznie tekst etykiet.
Samosprawdzanie
npm run self-check # detektor js na własnym kodzie; kod wyjścia 1 przy nowych odstępstwachNarzędzie przechodzi własny test i nie zgłasza nic — mimo że dziewięć jego
plików samodzielnie czyta process.argv, a parsowanie flag ma pięć osobnych
implementacji; deps działa już na JavaScripcie, ale tej wady nie widzi, bo
mierzy omijanie istniejącej warstwy, a tu żadnej warstwy nie ma.
To rozróżnienie jest sednem, nie wymówką. deps odpowiada na pytanie „N miejsc
idzie przez X, K bezpośrednio". Pięć kopii parsowania flag to inna wada:
duplikat bez warstwy. via = 0, więc regule nie ma czego porównać —
w przebiegu na własnym kodzie widać to wprost jako operacji opakowanych=0.
Wykrycie tego wymaga reguły szukającej powtórzonego kodu, a nie odstępstwa od
konwencji; to inne narzędzie.
Port deps na gramatykę JS/TS jest sprawdzony i działa — na próbce
kontrolnej (pięć modułów przez safeio.mjs, jeden wprost do fs.readFileSync)
zgłasza rozjazd poprawnie. Jedno ograniczenie zmierzone przy okazji:
- Reguła cienkiego opakowania słabo przenosi się na JavaScript. Wymaga, by
nazwa opakowania zawierała nazwę operacji —
movePathWithRetry⊃movedziała, alereadFileWithRetry⊅readFileSyncjuż nie. Nazwy API Node są długimi złożeniami, więc warunek rzadko bywa spełniony. Ta sama próbka kontrolna przechodzi dopiero po przemianowaniu opakowania nareadFileSyncWithRetry.
Zero zgłoszeń przy samosprawdzaniu jest więc miarą zasięgu reguł, nie jakości kodu — i teraz wiadomo dokładnie, której reguły brakuje.
Detektor js — JavaScript i TypeScript
Jedna gramatyka (tree-sitter-typescript) na oba języki: TypeScript jest
nadzbiorem JavaScriptu. Sprawdzone na materiale — 40 plików .js i 10 .ts,
zero błędów parsowania. Skrypty inline wycinane są z HTML-a, z zachowaniem
przesunięcia linii, więc numery w raporcie wskazują linię w HTML-u.
Reguła sierota: nazwa wołana jak funkcja, której strona nie zna. Strona zna
własne definicje ze swoich skryptów inline, globalne z ładowanych plików
<script src> oraz wbudowane przeglądarki i języka. To nie jest reguła
z listy dobrych praktyk — wynik zależy od tego, które pliki dana strona
ładuje, więc bez wiedzy o projekcie nie da się jej postawić.
| rewizja | stron | zgłoszeń | prawdziwych | trafność |
|---|---|---|---|---|
| przed naprawą (c45f7a6^) | 88 | 1 | 1 | 100% |
| bieżąca | 88 | 0 | — | kontrola negatywna |
Znaleziona sierota to closeAiReqLightbox w app.html:9464 —
dokładnie ta linia, którą usunął commit c45f7a6.
Pierwszy przebieg dał 5 zgłoszeń, z czego 4 fałszywe. Przyczyna była w moim
wycinaniu skryptów: słowo <script> napisane w komentarzu HTML brałem za
otwarcie bloku i parowałem z zamknięciem sto linii dalej, przez co CSS i proza
trafiały do parsera jako JavaScript. Komentarze są teraz wygaszane przed
szukaniem <script>, ze spacjami zamiast treści, żeby nie rozjechać numeracji.
Wyciszanie
Bez tego przy trzecim uruchomieniu człowiek przewija te same zgłoszenia i przestaje uruchamiać narzędzie. Dwie drogi, obie działają równocześnie.
Komentarz w kodzie — decyzja stoi tam, gdzie zapadła, i wędruje razem z kodem przez przenosiny i scalenia:
closeAiReqLightbox(); // odd-one-out: ok — celowo, funkcja wraca w następnym wydaniuSzukany jest w linii zgłoszenia i w linii powyżej, więc obie formy są naturalne. Powód po myślniku trafia do raportu, żeby dało się przejrzeć, co zostało wyciszone i dlaczego.
Plik .odd-one-out.json — do decyzji zbiorczych, po unitId (całe miejsce)
albo id (pojedyncza reguła); patrz Wykluczenia i wyciszenia.
Wyciszenie nie usuwa miejsca z populacji — znika tylko z raportu, więc nie osłabia reguły, która je złapała.
Różnica między uruchomieniami — domyślna
Pytanie brzmi nie „co jest nie tak", tylko „co jest nie tak od ostatniego razu". Dlatego różnica jest zachowaniem domyślnym, a nie osobnym poleceniem:
odd-one-out java ./src/main/java --json .odd-one-out/java.json # 1. raz: pełna lista
# ...praca nad kodem...
odd-one-out java ./src/main/java --json .odd-one-out/java.json # 2. raz: tylko nowe
odd-one-out java ./src/main/java --json .odd-one-out/java.json --all # cała listaGdy plik wskazany przez --json już istnieje, jest czytany jako poprzedni
przebieg, a raport pokazuje tylko nowe i zmienione zgłoszenia. Nagłówek
podaje pełny bilans:
roznica wobec poprzedniego przebiegu: NOWE=0 ZNIKNELO=0 ZMIENIONE=0 bez zmian=12--all przywraca pełną listę. Osobne polecenie odd-one-out diff <a> <b>
zostaje do porównywania dwóch dowolnych zapisów.
Kod wyjścia
0 — brak nowych odstępstw. 1 — są nowe. Dotyczy każdego detektora, nie tylko
polecenia diff, więc nadaje się wprost do CI:
odd-one-out java ./src/main/java --json .odd-one-out/java.json || echo "nowe odstępstwa"Przy pierwszym uruchomieniu (brak zapisu) wszystkie zgłoszenia są nowe, więc kod
to 1. Przy kolejnym bez zmian w kodzie — 0. Błąd użycia (zła ścieżka, brak
argumentu) kończy się kodem 2.
Czego detektor sql już nie liczy — zmiana w 0.2.0
Dwa rodzaje zgłoszeń są nadal wypisywane i przestają wpływać na kod wyjścia.
Funkcja zwracająca trigger albo event_trigger nie jest odstępstwem.
PostgreSQL sprawdza EXECUTE na funkcji wyzwalacza przy CREATE TRIGGER, a nie
przy odpaleniu wyzwalacza; wywołanie wprost kończy się błędem 0A000 trigger
functions can only be called as triggers. Dla nich revoke bez pary jest
stanem docelowym, nie połową pary — a poprawka, którą narzędzie dotąd
podpowiadało, odwracała migrację utwardzającą. Przebieg wypisuje, ile funkcji
pominięto, żeby „niesprawdzone" i „sprawdzone, czysto" nie wyglądały tak samo.
Zgłoszenie naprawione późniejszą migracją idzie do osobnej sekcji i się nie
liczy. Naprawa jest już w katalogu, a czerwone budowanie na rzeczy naprawionej
uczy wyłączać narzędzie. Wypisujemy je pod nagłówkiem NAPRAWIONE W PÓŹNIEJSZEJ
MIGRACJI i trzymamy poza zapisem przebiegu — inaczej diff policzyłby je jako
nowe i kod wyjścia wróciłby tylną furtką.
Typ czytamy z create [or replace] function … returns …, które może stać
w innej migracji niż odebranie, więc czytany jest cały katalog. Gdy deklaracji
nie ma w skanowanym katalogu, typ jest nieznany i zgłoszenie zostaje —
wyciszenie czegoś, czego nikt nie sprawdził, jest gorsze niż jedno zgłoszenie
za dużo.
Szczegóły różnicy
Wyjście dzieli się na NOWE, ZNIKNĘŁO, ZMIENIONE (to samo miejsce,
inna siła dowodu — np. sup: 8 -> 9, conf: 0.8 -> 0.9, viol: 2 -> 1) i bez
zmian.
Odcisk zgłoszenia nie zawiera numeru linii. To jedyna decyzja, która tu naprawdę waży: numery przesuwają się przy każdej niezwiązanej edycji, więc gdyby wchodziły do odcisku, dopisanie importu na górze pliku kasowałoby wszystkie stare zgłoszenia i wystawiało je jako nowe. Odcisk stoi na tożsamości semantycznej: detektor + reguła + plik + kotwica.
Zmierzone: przesunięcie całego pliku o trzy linie i naprawa jednego z dwóch
odstępstw dały ZNIKNĘŁO=1, bez zmian=8 — żadnego fałszywego „nowego"
z powodu przesuniętych linii.
Dwie rzeczy, o których trzeba wiedzieć:
- Naprawa jednego odstępstwa potrafi wygenerować nowe. W demie dodanie
setOnErrorusunęło jedno zgłoszenie i utworzyło trzy — bo to miejsce ma terazsetOnError, ale nie masetCycleCount,setOnHaltedanisetOnEndOfMedia. To nie usterka, tylko własność miningu: naprawa zmienia populację, do której wszystko jest porównywane. - Przeniesienie klasy do innego pakietu zmienia ścieżkę, więc zgłoszenie
wyjdzie jako
NOWE+ZNIKNĘŁO. Świadomy kompromis — odcisk bez ścieżki zlewałby ze sobą różne miejsca.
Pary czystych akcesorów — odsiewane, włączone domyślnie
Kopanie reguł nie wie, co metoda robi, więc getName obok getBirthDate
wyglądało dokładnie jak stop obok dispose. Przeczytanie jednego pola bez
drugiego nie jest wadą; niezwolnienie odtwarzacza jest.
Reguła wypada tylko wtedy, gdy OBIE strony są czystym czytaniem. hasNext ->
next i getInputStream -> close zostają: czytanie w parze z czymś, co działa,
to właśnie kształt wart zgłoszenia.
Na własnym kodzie tego nie było widać, na cudzym rzucało się w oczy — i po to właśnie zestaw regresyjny chodzi po trzech obcych projektach:
| projekt | przed | po | usunięte | |---|---|---|---| | spring-petclinic (adnotacje, głównie czytanie) | 7 | 2 | 71% | | JSON-java (mały, zwykły) | 31 | 17 | 45% | | netty/common (niskopoziomowy) | 100 | 79 | 21% | | projekt autora (pełen zmian stanu) | 462 | 387 | 16% |
Nic prawdziwego nie przepadło. Pięć znanych odpowiedzi nadal przechodzi, czwórka
na czele rankingu bez zmian, a jedyny wpis, który wypadł z piątki, to
MediaView#getFitHeight -> getFitWidth w metodzie zoomBaseH() — czytanie
wysokości bez szerokości w metodzie, która z definicji liczy wysokość.
--accessors keep przywraca stare zachowanie.
Reguły A→B i B→A są zgłaszane obie i tak ma być. Wygląda to na duplikat,
a nie jest: „ma A, nie ma B" i „ma B, nie ma A" to dwa różne miejsca. Zmierzone
na wszystkich czterech projektach: miejsc zgłoszonych dwa razy dla tej samej
pary jest zero — w netty error -> info wskazuje linię 89, a info ->
error linię 79.
Odsiewanie „setter obok settera" — włączone domyślnie
Mechaniczne współwystąpienia wywołań konfiguracyjnych zajmowały pierwsze
dwanaście pozycji rankingu: setMinHeight -> setMinWidth,
initModality -> initOwner, setCycleCount -> play. Kolejność i komplet są tam
przypadkowe, a brak jednego z nich nie jest błędem.
Trzy sygnały, każdy włączany osobno (--odsiej 1, --odsiej 1,3,
--odsiej none):
- obie strony to zwykłe settery —
set[A-Z], ale niesetOn[A-Z]; - odbiornik powstał w tej samej jednostce (
new X()obok); - obie ustawiają stan — żadna nie jest zdarzeniowa ani czynnością cyklu
życia (
play,stop,dispose,close, …).
Granica jest tu istotna: setOnError zaczyna się od set, ale podpina obsługę
zdarzenia, a nie ustawia wartość, więc nie jest setterem dla żadnego z sygnałów.
| sygnał | zgłoszeń (odkrywanie, projekt autora) | pozycja Loading.java:397 |
|---|---|---|
| brak | 1086 | 88 z 99 |
| 1 | 645 | 56 |
| 2 | 708 | 70 |
| 3 | 628 | 55 |
| 1,2,3 | 462 | 47 z 99 |
Na cudzym kodzie (netty/common, 204 pliki) odsiewanie zdejmuje 101 → 100 — czyli praktycznie nic. Ten kod nie ma masowej konfiguracji setterami, którą sygnały 1 i 3 mają wycinać.
Wcześniejsza wersja tej tabeli podawała 161 zgłoszeń i pozycję 13 z 30. Te liczby mierzyły zepsuty sygnał 3, który definiował „ustawia stan" przez negację i ukrywał około trzystu poprawnych zgłoszeń, a na cudzym kodzie wycinał całą treść raportu. Po naprawie poprawa jest realna, ale znacznie skromniejsza: z pozycji 88 na 47, a nie na 13.
Żaden sygnał nie zabrał ani jednej znanej odpowiedzi.
MediaPlayer#dispose -> MediaPlayer#setOnError, Loading.java:397, :411,
Menu.java:5753/5754 przeżywają wszystkie trzy i ich złożenie.
W trybie zawężonym --only setOnError: 14 → 12 zgłoszeń; ranking skrócił się do 7 pozycji, a wszystkie cztery znane odpowiedzi weszły do pierwszej piątki.
Usunięte to Menu.java:2690 i Preview.java:498 — oba wcześniej ocenione jako
fałszywe, oba złapane sygnałem 2 (kompletna konfiguracja świeżo utworzonego
MediaPlayer).
Osobna słabość raportu, widoczna przy odkrywaniu: nagłówek scalonej pozycji
wybierany jest po ocenie, więc regułą wiodącą bywa szumowa (dispose ->
getStatus), a prawdziwa (dispose -> setOnError) ląduje w liście „także".
Odkrywanie par — zmierzone, szum zalewa wynik
Detektor java domyślnie odkrywa pary sam: dla każdego typu odbiornika
zbiera wszystkie wołane na nim metody i liczy każdą parę. --only <nazwy> jest
filtrem zawężającym do wybranej rodziny, nie warunkiem działania.
| próg | reguł | zgłoszeń | pozycji w rankingu | czas |
|---|---|---|---|---|
| --minsup 5 | 327 | 711 | 99 | 2,1 s |
| --minsup 3 (domyślny) | 615 | 1086 | — | 2,1 s |
Koszt nie jest problemem — 2,1 sekundy na 11 581 jednostkach. Obawa o dziesiątki tysięcy par się nie potwierdziła.
Szum jest problemem. Pierwsze dwanaście pozycji rankingu to w całości mechaniczne współwystąpienia setterów JavaFX:
1. [97] Button#setMinHeight -> Button#setMinWidth konwencja=97% populacja=36
2. [97] Stage#getIcons -> Stage#setScene konwencja=97% populacja=28
5. [96] Timeline#setCycleCount -> Timeline#play
7. [96] Stage#initModality -> Stage#initOwnerZweryfikowane trafienia Loading.java:397 i :411 lądują na pozycjach
73 i 74 ze 99. Trafność w pierwszej dziesiątce: 0%. Pozycja 1 ma przy
sobie 36 innych naruszonych reguł — jako zgłoszenie dla człowieka to nie jest
czytelne.
Progów nie dostrajałem pod ten pomiar. Wniosek jest inny: odkrywanie par
jest dobre do znalezienia rodzin reguł, których się nie znało (327 par
z typem odbiornika to materiał do przejrzenia), a nie do czytania zgłoszeń.
Do przeglądu zawężaj --only.
Znana para przy progu 5 znika
MediaPlayer#dispose -> MediaPlayer#setOnError nie znajduje się wśród
odkrytych przy --minsup 5 — jej wsparcie wynosi 3. Pojawia się dopiero przy
domyślnym --minsup 3 i wskazuje wtedy Menu.java:5754 oraz Loading.java:974.
Przy progu 5 znikają też oba prawdziwe zgłoszenia z Menu.java.
Innymi słowy: próg pięciu wystąpień, choć brzmi ostrożniej, kosztuje jedno z czterech znanych prawdziwych trafień. Domyślny próg pozostaje 3.
Typ odbiornika i aliasy
Dwie próby podniesienia trafności detektora java, obie zmierzone na tym samym
zbiorze (111 plików, reguła setOn*, zasięg lambda):
| wariant | zgłoszeń | prawdziwych | trafność | Loading.java:397/411 |
|---|---|---|---|---|
| baza | 15 | 3 | 20% | poz. 3 / 4 |
| + typ odbiornika | 14 | 4 | 29% | poz. 4 / 5 |
| + typ + aliasy | 14 | 3 | 21% | poz. 4 / 5 |
Typ odbiornika — zostaje włączony (--typy off wyłącza). Bez niego
mediaControl.setOnEndOfMedia() (klasa projektu, metoda bezargumentowa)
i mediaPlayer.setOnEndOfMedia(Runnable) liczą się jako ta sama pozycja.
Usunęło dokładnie te dwa fałszywe alarmy i wypromowało prawdziwe zgłoszenie
Menu.java:5753 z pozycji 10 na 3.
Rozpoznawanie typu ma dwa źródła: deklaracje w pliku oraz mapę
wyrażenie → typ zbieraną z całego projektu (MediaPlayer player =
mediaView.getMediaPlayer(); uczy, że to wyrażenie ma typ MediaPlayer).
Drugie źródło jest konieczne — bez niego odbiorniki będące wywołaniem metody
zostają nierozpoznane i wypadają z populacji razem z prawdziwymi
odstępstwami.
Aliasy — zmierzone, pogarszają wynik, domyślnie wyłączone (--aliasy on
włącza). Idea jest słuszna: final MediaPlayer toDispose = player; to ten sam
obiekt. Poprawka usuwa dokładnie ten fałszywy alarm, dla którego powstała
(Loading.java:974), ale przypisanie wywołań do jednostki deklaracji scala też
wywołania niezwiązane i tworzy nowe reguły na metodach zapytujących
(setOnError -> getStatus). Bilans: −1 fałszywy alarm, +2 nowe.
Zasięg pary
Jak blisko siebie muszą stać dwa wywołania, żeby liczyć się jako para.
--scope file|method|lambda (domyślnie lambda).
| zasięg | jednostka | zgłoszeń | prawdziwych | trafność |
|---|---|---|---|---|
| file | plik + odbiornik | 9 | 2 | 22% |
| method | metoda/konstruktor + odbiornik | 14 | 1 | 7% |
| lambda | najgłębsza funkcja + odbiornik | 15 | 2 (w 3 zgłoszeniach) | 20% |
Zmierzone na 111 plikach, regule setOn*. „Prawdziwe" = defekty, które prześledziłem
w kodzie: brak zwolnienia playera w Loading (dwa miejsca) i dispose() bez
zerowania handlerów w Menu.
Ta tabela pochodzi sprzed wprowadzenia typu odbiornika — jest punktem wyjścia, nie stanem bieżącym. Domyślny zasięg
lambdadaje dziś 29%, a nie 20%. Porównanie trzech zasięgów między sobą pozostaje ważne, bo wszystkie trzy zmierzono na tej samej wersji.
Zwężanie zasięgu nie poprawia trafności — poprawia ją poszerzanie. Fałszywe
alarmy tej klasy biorą się stąd, że obsługa stoi o poziom wyżej niż wywołanie;
wąski zasięg tego nie widzi i zgłasza brak. Zasięg file znajduje te same
defekty przy 40% mniejszej liczbie zgłoszeń. Zasięg method jest najgorszy:
gubi Menu.java:5754 całkowicie, bo reguła dispose->setOnError traci przy tym
podziale wsparcie.
Domyślny zasięg został przy lambda — różnica 22% vs 20% na jednym projekcie
i jednej rodzinie reguł to za cienki dowód, żeby zmieniać zachowanie. Do
przeglądu warto uruchomić --scope file.
Stabilność wzorca — czwarty składnik oceny, domyślnie wyłączony
odd-one-out rank ... --stabilnosc
Wzorzec obecny w każdym podzbiorze populacji jest pewniejszy niż taki, który powstaje dopiero z całości — ten drugi często oznacza regułę zlepioną z kilku niezależnych zwyczajów panujących w różnych częściach projektu.
Populacja dzielona jest na cztery podzbiory po pliku (klasa nie rozjeżdża
się między podzbiory), a reguła liczona w każdym osobno oraz narastająco na
prefiksach. stab to średnia z obu udziałów.
To jest sprawdzenie, nie zmiana populacji. Reguły są wydobywane z całego
zbioru; podzbiory służą wyłącznie do policzenia, jak równo wzorzec się rozkłada.
Dzielenie populacji, na której pracuje detektor, pogarsza wynik — zmierzone
przy zasięgu method (7%).
Zmierzone:
| pozycja | bez --stabilnosc | z --stabilnosc | stab |
|---|---|---|---|
| Screen.java:1496 (szum) | 1 | 1 | 1,00 |
| Screen.java:9861 (szum) | 2 | 2 | 1,00 |
| Menu.java:5753 (prawdziwe) | 3 | 5 | 0,71 |
| Loading.java:397 (zweryfikowane) | 4 | 3 | 0,83 |
| Loading.java:411 (zweryfikowane) | 5 | 4 | 0,83 |
Pozycja zweryfikowanych trafień drgnęła — w górę o jedno miejsce. Ale ruch
nie jest poprawą: awans wziął się stąd, że w dół spadło inne prawdziwe
zgłoszenie (Menu.java:5753), a dwa fałszywe alarmy z czoła listy są na ten
składnik odporne — ich reguły są idealnie stabilne (1,00). Liczba prawdziwych
zgłoszeń w pierwszej trójce i piątce nie zmieniła się wcale.
Dlatego zostaje wyłączone domyślnie: ruch w rankingu to nie to samo co poprawa rankingu. Składnik jest poprawny i tani — warto spróbować w projekcie, gdzie reguły nie są tak jednorodnie stabilne jak tutaj.
Wywołania HTTP bez timeoutu — zmierzone, przesłanka nie trzyma
Detektor Pythona miał dostać regułę, która wygląda oczywiście: requests.get
bez timeout= może wisieć w nieskończoność, więc jeśli dziesięć wywołań
w projekcie ma timeout, a jedno nie ma, to jedno jest podejrzane.
Przesłankę zmierzyłem, zanim reguła powstała, na czterech prawdziwych projektach:
| projekt | wywołań HTTP | z timeoutem | |---|---|---| | redash | 43 | 1 | | prefect | 38 | 1 | | certbot | 5 | 1 | | sherlock | 4 | 4 |
W redash i prefekcie konwencją jest brak timeoutu. Reguła „dziesięć ma, jedno nie ma" nie ma się tam czego uchwycić — a czytana dosłownie zgłosiłaby jako odstępstwo to jedno wywołanie, które timeout MA. W sherlocku wszystkie cztery go mają i nie ma czego znajdować.
Reguła nie powstała. Jako sprawdzenie uniwersalne („każde wywołanie HTTP musi mieć timeout") byłaby zupełnie dobrą regułą lintera i są lintery, które to robią — ale nie jest zdaniem o konwencji TEGO projektu, a tylko takie zdania to narzędzie wypowiada.
Odłożone — i czego każdemu brakuje
Wszystko, co nie jest zbudowane, leży tutaj, w jednym miejscu, z konkretną rzeczą, która by je odblokowała. Żadna z nich nie jest obietnicą; dwie są pomysłami, a jedna jest luką w pomiarze.
Detektor Pythona — napisany, zmierzony, niewydany
Model z Javy został przeniesiony na Pythona: te same jednostki, to samo
wydobywanie reguł, te same progi. Trudny jest odbiornik — Python nie deklaruje
typów, więc „ten sam obiekt" rozpoznaje się po nazwie zmiennej w zasięgu
funkcji, ze znacznikiem z konstruktora (sock = socket.socket(...) znaczy
sock jako socket), z listy importów dla odbiorników modułowych albo z klauzuli
with ... as. Co przy tym przepada, jest wypisane: obiekt przychodzący
parametrem, czytany z pola albo rozpakowany z indeksu wpada do ?, tam gdzie
w Javie byłaby deklaracja do przeczytania.
Detektor działa. Na siedmiu prawdziwych projektach, przy progach domyślnych, bez żadnego strojenia:
| projekt | plików .py | jednostek | reguł, które przeszły | naruszeń | |---|---|---|---|---| | django | 2930 | 68 971 | 552 | 931 | | prefect | 1819 | 54 341 | 336 | 596 | | scrapy | 487 | 8 628 | 72 | 128 | | celery | 423 | 13 526 | 71 | 139 | | paramiko | 70 | 2 869 | 71 | 126 | | urllib3 | 81 | 3 447 | 39 | 57 | | certbot | 33 | 920 | 6 | 10 |
Reguły czytają się sensownie — Signal#connect -> Signal#disconnect w celery,
ExecutionEngine#open_spider_async -> close_spider_async w scrapy,
HTTPConnection#request -> HTTPConnection#getresponse w urllib3 przy 19 na 22.
Model ma w Pythonie materiał i to nigdy nie było pytaniem.
NIE JEST WYDANY, BO NIE MA ZNANEJ ODPOWIEDZI. Każdy detektor tutaj zasługuje
na swoje miejsce tym, że wskazuje defekt, który ktoś naprawdę naprawił,
sprawdzony na rewizji sprzed naprawy (patrz test/known-answers.mjs). Python
takiej nie ma. Dwie próby znalezienia jej ręcznie padły z powodów wartych
zapisania: w docker-py jedyne requests.get bez timeoutu było JEDYNYM
wywołaniem HTTP w repozytorium — brak populacji, więc nie ma konwencji, od której
można odstawać; w streamlink i urllib3 zasób, którego zamknięcie dopisano, nigdy
nie był odbiornikiem w naprawianej funkcji, więc model par nie miał czego parować.
Wtedy kolejność została odwrócona: zamiast szukać naprawy i pytać, czy narzędzie ją widzi, puścić narzędzie na rewizji z 2022 i zapytać, czy cokolwiek z tego, co zgłosiło, zostało później poprawione.
| repozytorium | naruszeń w 2022 | naprawionych | nadal narusza | zniknęło z kodem | commitów od tamtej pory | |---|---|---|---|---|---| | urllib3 | 37 | 0 | 20 | 15 | 656 | | paramiko | 137 | 0 | 107 | 30 | 368 | | docker-py | 86 | 0 | 86 | 0 | 245 | | razem | 260 | 0 | 213 | 45 | |
213 z 260 naruszeń przeżyło trzy lata i 1269 commitów nietkniętych. To jest uczciwa miara tego, co ta klasa narzędzi zgłasza: w większości nie defekty. Własne 18,1% PR-Minera mówi to samo jedną liczbą.
Jeden przypis o metodzie, bo o mało nie poszło inaczej. Pierwszy przebieg
sprawdzianu zgłosił dwa naruszenia urllib3 jako naprawione. Nie były: sprawdzian
dopasowywał miejsca po trójce (plik, nazwa funkcji, odbiornik), a w tym pliku są
CZTERY funkcje o nazwie socket_handler i tylko jedna woła brakującą metodę —
przy czym wołała ją już w 2022. Commit, który ruszył tamtą okolicę,
d560e21d "Consume connections better in socket-level tests", to infrastruktura
testów, nie naprawa defektu. Przyrząd pomiarowy, który zgadza się z hipotezą, to
pierwsza rzecz, której nie należy ufać.
Detektor szwów — pomysł, nie pomiar
Ta sama zasada przyłożona do testowalności. Nie „wstrzykuj zależności" — to reguła uniwersalna, a takich to narzędzie nie stawia. Raczej: piętnaście klas dostaje zależność z góry, ta jedna tworzy ją sama.
Czego by szukał — każde to brakujący szew w miejscu, w którym reszta projektu go ma:
- konstruktor wołany wprost wewnątrz metody, gdzie indziej obiekt przychodzi z zewnątrz
- wywołanie statyczne tam, gdzie reszta kodu trzyma pole
- zegar albo system plików sięgnięty ze środka logiki
- singleton czytany z wnętrza funkcji zamiast podany
Brakuje znanej odpowiedzi. Każdy detektor, który tu jedzie, wskazuje defekt, który ktoś naprawdę naprawił, sprawdzony na rewizji sprzed naprawy. Ten nie ma żadnej, więc jest hipotezą, a nie detektorem.
Python jest pomiarem tego, ile taki brak kosztuje. Detektor został napisany, przeszedł progi na siedmiu projektach, a w trzech repozytoriach z pełną historią zgłosił 260 naruszeń, z których ani jedno nie zostało przez nikogo poprawione. Bez znanej odpowiedzi nie da się odróżnić detektora, który działa, od takiego, który tylko coś wypisuje — a wypisują tak samo.
Mutacje poza src/snapshot.mjs
npm run full uruchamia Strykera. Zmierzony jest dokładnie jeden plik:
src/snapshot.mjs — 316 mutantów, 216 przeżyło. To wynik 31,65% przy
czterech zestawach jako sędzim i 23,42% przy samych testach złotych. Każdy inny
plik w src/ jest niezmierzony, więc o nich nie wiadomo nic.
Podejrzenie mówiło, że większość ocalałych siedzi w wypisywanym tekście.
Zostało zmierzone na drugim pliku i jest fałszywe. src/oddone.mjs — detektor,
który daje 387 z 391 zgłoszeń na prawdziwym projekcie — poszedł jako następny:
910 mutantów, 468 zabitych, 14 timeoutów, 428 ocalałych, wynik 52,97%
w 5 godzin 33 minuty, przy tym samym sędzim z czterech zestawów.
Z 428 ocalałych 2 siedzą w wypisywanym tekście, a 426 w logice. Domysł był błędny i błędnie pocieszający: sugerował, że niezmierzona część jest kosmetyką.
| obszar pliku | mutantów zabitych | z całości | obszar | |---|---|---|---| | stabilność wzorca | 50 / 56 | 89% | ocena | | kopanie reguł | 24 / 29 | 83% | rdzeń | | jednostki i odbiorniki | 43 / 70 | 61% | rdzeń | | flagi i ustawienia | 25 / 56 | 45% | | | rozpoznawanie typów | 42 / 133 | 32% | włączone domyślnie | | filtr akcesorów | 34 / 116 | 29% | włączone domyślnie | | aliasy | 15 / 201 | 7% | wyłączone domyślnie |
Połowa wszystkich ocalałych (213 z 428) siedzi w gałęzi aliases, wyłączonej
domyślnie. Żaden test jej nie włącza, więc nic nie może tam zabić mutanta — kod
jest zmierzony jako nieprzetestowany, bo jest nieprzetestowany, a jest
nieprzetestowany, bo został zmierzony jako szkodliwy i wyłączony. To jest spójne,
nie niepokojące.
Dwie liczby, które miały znaczenie, to rozpoznawanie typów na 32% i filtr akcesorów na 29% — oba włączone domyślnie i oba kształtujące każde zgłoszenie. Przyczyna była w materiale, nie w testach: lista akcesorów wymienia trzydzieści nazw, a wzorce używały trzech.
Naprawione pisaniem wzorców, nie testów, i zmierzone ponownie:
| obszar | przed | po | zmiana | |---|---|---|---| | rozpoznawanie typów | 42 / 133 — 32% | 89 / 132 — 67% | +35 punktów | | filtr akcesorów | 34 / 116 — 29% | 89 / 116 — 77% | +48 punktów | | oba razem | 76 / 249 — 31% | 178 / 248 — 72% | +41 punktów |
Trzydzieści pięć nowych plików wzorcowych trzyma pary akcesorów, które
przeszłyby progi, gdyby filtr przestał działać — cztery jednostki z wzorcem,
jedna odstająca. Dopóki filtr działa, milczą; w chwili gdy mutant wytnie nazwę
z listy, reguła wychodzi i test złoty pada. Cztery kolejne wzorce pokrywają trzy
źródła typu odbiornika, ustawione tak, że dwa różne typy dzielą nazwę metody: bez
rozpoznawania typów zlewają się w ?#open, liczba naruszeń przekracza
--maxviol i zgłoszenie znika.
Uwaga o pierwszej próbie, bo dała wynik idealny, a wynik idealny był właśnie problemem. Pierwszy pomiar zgłosił 463 mutantów, wszystkie zabite, 100%, w 21 sekund — przy sędzim trwającym osiem. Jeden mutant, który wcześniej przeżył, został zastosowany ręcznie i bramka przeszła, co przeczyło raportowi. Przyczyna: nowe wzorce nie były jeszcze zacommitowane, a Stryker buduje piaskownicę z plików znanych gitowi. W piaskownicy siedział stary zestaw wzorców, test złoty padał tam przy każdym mutancie niezależnie od mutacji, a każda porażka liczyła się jako zabity mutant. Przyrząd, który zgadza się z hipotezą, jest pierwszą rzeczą do sprawdzenia — liczby powyżej pochodzą z przebiegu po zacommitowaniu wzorców.
Nadal przeżywa 70 mutantów: 43 w rozpoznawaniu typów i 27 w filtrze akcesorów.
Skupiają się w ścieżce --only, której nie ćwiczy żaden wzorzec, w przypadku
szczególnym var oraz w obcinaniu typów generycznych i nawiasów tablicowych.
| plik | mutantów | zabitych | ocalałych | wynik | czas |
|---|---|---|---|---|---|
| src/snapshot.mjs | 316 | 72 + 2 | 242 | 23,42% | 1 m 57 s (sam golden) |
| src/snapshot.mjs | 316 | 82 + 18 | 216 | 31,65% | 118 m (cztery zestawy) |
| src/oddone.mjs | 910 | 468 + 14 | 428 | 52,97% | 333 m (cztery zestawy) |
Cała reszta src/ pozostaje niezmierzona.
Powód, dla którego kończy się na jednym pliku, to koszt: ten jeden zajął
118 minut. Całe src/ to byłby prawie cały dzień i dlatego mutacje siedzą
za npm run full, a nie w npm test.
Ranking wedle formuły JADET — zmierzony, pogorszył
Ranking premiuje liczebność i to jest prawdziwa słabość: trzy zweryfikowane defekty stały na pozycjach 34, 41 i 42 ze 167, wszystkie z identyczną oceną 32. Identyczna ocena to nie ranking, tylko lista.
JADET (Wasylkowski, Zeller, Lindig, 2007) ocenia naruszenie jako u × s / v — unikalność wzorca razy wsparcie, przez liczbę naruszeń — i wymaga, by odstępstwo było co najmniej dziesięć razy rzadsze niż wzorzec. Zostało to zasymulowane na tych samych zapisach, zanim tknąłem linijkę narzędzia.
| znana odpowiedź | obecna | u × s / v | u × s / v + wymóg 10× |
|---|---|---|---|
| closeAiReqLightbox (js) | 1 | 1 | 1 |
| 20260901130000 (sql) | 5 | 6 | 6 |
| io.thorntail:javafx (pom) | 8 | 18 | wypadła |
| Menu.java:5753/5754 (java) | 34 | 47 | wypadła |
| Loading.java:397 (java) | 41 | 61 | wypadła |
| Loading.java:411 (java) | 42 | 62 | wypadła |
Każda pozycja, na której zależało, pogorszyła się. Trzy odpowiedzi z java —
te zakopane za głęboko, żeby ktokolwiek doczytał, czyli cały powód tej próby —
spadły z 34/41/42 na 47/61/62. Lista skurczyła się ze 167 pozycji do 13 przy
wymogu 10×, a wśród skasowanych znalazły się cztery z pięciu znanych odpowiedzi.
Przyczyna jest arytmetyczna, nie implementacyjna. u × s / v premiuje
wsparcie, a tutaj prawdziwe defekty stoją na małych populacjach:
Menu.java:5754 na wzorcu trzymanym 4 razy, Loading.java:397 na trzymanym 8
razy. Szum stoi na dużych — Button#setOnMouseEntered -> setStyle jest trzymany
45 razy na 49. Obecna formuła ogranicza nagrodę za liczebność do dziesięciu
przykładów (min(1, sup/10)) i właśnie ten limit nie pozwala zakopać defektów
o małej populacji. Surowe s z JADET-a go znosi.
Wymóg 10× zawodzi z tego samego powodu, tylko mocniej: 29 z 31 zgłoszeń stojących za pięcioma znanymi odpowiedziami ma liczbę naruszeń większą niż jedna dziesiąta wsparcia. Z sześciu miejsc przechodzą dwa. Ta reguła powstała dla zbioru tysięcy projektów, gdzie wzorzec jest trzymany setki razy; tutaj cała populacja reguły to 4 do 15 miejsc, bo to jedna aplikacja desktopowa.
Nieprzyjęte. W src/ nie zmieniło się nic — symulacja wystarczyła do decyzji,
a pomiar kosztował mniej niż kosztowałaby zmiana.
Liczby wskazują natomiast na co innego: scalanie zachowuje najwyższą ocenę spośród reguł naruszonych w danym miejscu, więc miejsce naruszające 36 reguł ma trzydzieści sześć losowań o wysoką ocenę, a naruszające jedną — jedno. To jest strukturalna przewaga mechanicznego współwystępowania i to jest następna rzecz warta zmierzenia — ale to inny pomysł i nie został jeszcze zmierzony.
Kara za wielość reguł naruszonych w jednym miejscu — zmierzona, obalona
Druga hipoteza o rankingu, tym razem wzięta z materiału, a nie z pracy naukowej. Scalanie zachowuje najwyższą ocenę spośród reguł naruszonych w jednym miejscu, więc miejsce naruszające 36 reguł ma trzydzieści sześć losowań o wysoką ocenę, a naruszające jedną — jedno. Mechaniczne współwystępowanie produkuje długie listy naruszonych reguł; zatem — tak szło rozumowanie — dzielenie przez ich liczbę powinno zepchnąć szum w dół.
Diagnoza obaliła to, zanim symulacja się skończyła.
| miejsce | naruszonych reguł | werdykt |
|---|---|---|
| closeAiReqLightbox (js) | 1 | prawdziwe |
| 20260901130000 (sql) | 1 | prawdziwe |
| io.thorntail:javafx (pom) | 1 | prawdziwe |
| Loading.java:397 / :411 (java) | po 4 | prawdziwe, zweryfikowane |
| Menu.java:5753/5754 (java) | 21 | prawdziwe, zweryfikowane |
Znane odpowiedzi TO SĄ miejsca o wielu regułach. To nie przypadek i z perspektywy
czasu jest oczywiste: jedno brakujące wywołanie łamie każdą regułę
współwystępowania, która je zawierała. MediaPlayer, który jest zatrzymany, ale
nigdy zwolniony i nigdy nie ma wyczyszczonych uchwytów, narusza
stop -> dispose, stop -> setOnError, stop -> setOnReady i osiemnaście
innych naraz. Liczba naruszonych reguł mierzy, jak dużą część cyklu życia obiektu
pominięto — to dowód ZA defektem, nie przeciw.
Zasymulowane mimo to, na tych samych zapisach:
| znana odpowiedź | obecna | ÷ n | ÷ √n | ÷ (1+log₂n) |
|---|---|---|---|---|
| closeAiReqLightbox (js) | 1 | 1 | 1 | 1 |
| 20260901130000 (sql) | 6 | 2 | 2 | 2 |
| io.thorntail:javafx (pom) | 10 | 4 | 4 | 4 |
| Menu.java:5753/5754 (java) | 34 | 167 — ostatnia | 160 | 159 |
| Loading.java:397 (java) | 41 | 138 | 85 | 103 |
| Loading.java:411 (java) | 42 | 139 | 86 | 104 |
Najmocniejszy zweryfikowany defekt w projekcie ląduje na ostatnim miejscu ze 167.
Odwrotność też została zmierzona i też nie jest odpowiedzią. Jeśli wiele
naruszonych reguł jest dowodem, to trzeba je nagradzać: × n przesuwa
Menu.java:5754 z 34 na 4, a Loading.java:397/411 z 41/42 na 19/20. Ale na
czele tej listy stoją VideoAnalyzerPro.java:9868 (27 reguł) i :1503 (12) —
czyli jednostka bindPlayButtonToPlayerStatus oraz lambda z linii 1486, dwa
miejsca, które ten dokument już opisuje jako fałszywe alarmy (patrz „Znane
ograniczenie: obsługa o poziom wyżej niż wywołanie"). Ranking, którego pierwsza
i trzecia pozycja to dwa znane fałszywe alarmy, nie jest poprawą — a przyjęcie go
byłoby dopasowaniem formuły do pięciu znanych odpowiedzi, czyli dokładnie tym,
czego ten projekt odmawiał przez cały czas.
Żadna nieprzyjęta. W src/ nie zmieniło się nic. Dwie odrzucone hipotezy
z pomiarem za sobą, a ranking nadal premiuje liczebność zamiast ciężaru. To
pozostaje problemem otwartym: trzy odpowiedzi z java siedzą na pozycjach 34, 41
i 42 ze 167 z identyczną oceną 32, a identyczna ocena to lista, nie ranking.
MUBench — sprawdzony, nie do uruchomienia tutaj, a zbiór jest trafiony
Bez publicznego zbioru odniesienia liczby trafności powyżej porównują się z jedną pracą z 2005 roku i z niczym więcej. MUBench (Amann i inni, TU Darmstadt 2015–2018, od tego czasu utrzymywany przez Svena Amanna) jest zbiorem odniesienia dla tej klasy narzędzi: wyselekcjonowany zestaw prawdziwych nadużyć API, potok uruchamiający na nim detektory oraz precyzja i pełność liczone po ręcznej recenzji.
Trzy blokady, każda wymierzona, nie założona.
- Detektor musi być wykonywalnym JAR-em. Nie „czymkolwiek, co się uruchamia" — MUBench wymaga JAR-a z wbudowanym MUBench Runnerem jako punktem wejścia, budowanego wtyczką Maven assembly. odd-one-out to CLI w Node.
- Nie ma tu czym go zbudować.
javanie jest na PATH imvnnie jest na PATH. - Potok chodzi w Dockerze, a demon nie działa. Klient jest zainstalowany —
wersja 29.7.2 — i odpowiada
failed to connect to the docker API at npipe:////./pipe/docker_engine. Poza tym obrazsvamann/mubench:stablejest zbudowany pod detektory javowe: nawet z JAR-em-owijką musiałby w nim być Node, żeby owijka miała co wołać.
Zbiór natomiast jest trafiony — i ten pomiar sam w sobie coś znaczy. Pobrany bez Dockera (klon rzadki, bez blobów, 1,8 MB) i policzony:
| kategoria naruszenia | ile |
|---|---|
| missing/call | 128 |
| missing/condition/value_or_state | 74 |
| redundant/call | 49 |
| missing/condition/null_check | 28 |
| missing/exception_handling | 27 |
| cała reszta | 14 |
128 z 320 naruszeń to brak wywołania — dokładnie ta klasa, którą wyraża model par. MUBench nie jest zbiorem obok tematu, tylko na temat. To warto wiedzieć, nawet jeśli nie da się go tutaj uruchomić.
Droga częściowa istnieje i została odrzucona na liczbach. Bez potoku dałoby się sklonować każdy projekt na rewizji sprzed naprawy i sprawdzić, czy detektor wskazuje ten plik i tę metodę — bez Dockera, Javy i Mavena, bo to narzędzie czyta źródła, zamiast je kompilować. Z 227 nadużyć 128 to brak wywołania, 110 z nich leży w repozytoriach git, a 54 mają sha naprawy razem z plikiem i metodą.
41 z tych 54 pochodzi z jednego projektu, Joda-Time — 76%. Liczba zrobiona w ten sposób brzmiałaby jak porównanie ze zbiorem odniesienia, będąc w trzech czwartych pomiarem jednej biblioteki. Nie miałaby też definicji precyzji i pełności, które MUBench stosuje, ani wbudowanej w niego recenzji ręcznej. Liczba, która brzmi jak wynik z benchmarku, nim nie będąc, jest gorsza niż brak liczby.
Nieuruchomione. Blokady są wypisane wyżej po to, żeby ktokolwiek z Dockerem, Javą i Mavenem mógł to podjąć w miejscu, w którym się zatrzymało: potrzebny jest JAR-Runner wołający Node oraz obraz, w którym Node się znajduje.
Wiek odstępstwa — zmierzony, nie pomógł, domyślnie wyłączony
odd-one-out rank ... --wiek <katalog-repo>
Sygnał: odstępstwo nowsze niż linie zgodne z wzorcem jest podejrzane — najstarsze jest zwykle oryginałem, od którego reszta odeszła świadomie. Działa wyłącznie jako podbicie oceny (×1,3); nic nie jest na tej podstawie usuwane ani obniżane.
Zmierzone na tym samym zbiorze:
| | bez wieku | z wiekiem |
|---|---|---|
| pozycja Loading.java:397 (zweryfikowane) | 3 | 3 |
| pozycja Loading.java:411 (zweryfikowane) | 4 | 4 |
| pozycja Screen.java:9861 (szum) | 2 | 1 |
Nie ruszyło prawdziwych trafień, a wypromowało jedno fałszywe. Przyczyna
jest ta sama, przed którą ostrzega zastrzeżenie o git blame, tylko w wersji
skrajnej: repozytorium ma 235 commitów, ale wszystkie 1116 linii
Loading.java noszą datę jednego commitu importującego projekt. Wiek mierzy
wtedy moment wrzucenia kodu do gita, a nie moment jego napisania — prawdziwe
odstępstwa mają dokładnie tę samą datę co wzorzec i nie dostają podbicia, a
przypadkowo dotknięte później linie dostają.
Dlatego --wiek zostaje wyłączone domyślnie. Ma szansę działać w repozytorium
z ciągłą historią, gdzie kod powstawał w gicie od początku — tu nie działa i nie
udaję, że działa.
Klasy JavaFX — sprawdzone osiem zgłoszeń, zero defektów
Narzędzie puszczono na aplikację JavaFX na Windows: 111 plików Javy, 61 729 linii. Wynik: 518 reguł, 164 zgłoszenia.
Osiem najwyżej ocenionych sprawdzono, czytając kod. Żadne nie okazało się defektem.
| # | reguła | dlaczego ją wypuściła |
|---|---|---|
| 1-3 | Stage#setOnHidden, Stage#getIcons, Stage#setOnShown w trzech plikach | metody konfiguracji okna; każde miejsce ustawia inny podzbiór |
| 4 | Region#getTranslateY -> Region#setOpacity | odczyt jest punktem wyjścia animacji, a ustawia ScaleTransition |
| 5 | TextField#setPrefWidth -> TextField#textProperty | pomocnik od wymiarów pola |
| 6 | StringBuilder#length -> StringBuilder#append | zły typ odbiornika: zmienna jest String, nie StringBuilder |
| 10 | HttpResponse#statusCode -> HttpResponse#body | wylogowanie rozstrzyga po kodzie HTTP, ciała nie ma czego czytać |
| 11 | Files#createFile -> Files#exists | istnienie sprawdzone linijkę wyżej, przez Files.isRegularFile |
| 15 | zoomBaseW -> videoFitHeightForStage | liczy szerokość; bliźniacze zoomBaseH woła wersję dla wysokości |
Przyczyna
„Na tym odbiorniku metoda A zwykle występuje obok metody B, a tutaj nie
występuje” — tak brzmi reguła. Przy API budowniczych (Stage,
MediaPlayer, Region, TextField) współwystępowanie nie jest konwencją.
Jest statystyką tego, co w danym miejscu akurat było potrzebne: jedno okno
ustawia ikonę i scenę, drugie tylko podpina zdarzenie, trzecie tylko rozmiar.
Model par nie odróżnia „wszyscy tak robią, bo tak trzeba” od
„wszyscy tak robią, bo akurat tego potrzebowali”.
Stąd zdanie ogólniejsze, i to ono jest tu właściwym wnioskiem: model par ma
sens tam, gdzie współwystępowanie jest OBOWIĄZKOWE, a nie zwyczajowe. revoke
bez grant execute w tej samej migracji — reguła detektora sql — to relacja
wymuszona: brak drugiego członu jest błędem z definicji. Stage#getIcons bez
Stage#setScene nie jest niczym; to dwie niezależne czynności, które w
statystyce stoją obok siebie. Im bliżej API jest konfiguracji, tym mniej z tej
statystyki wynika.
Dwa zgłoszenia z tabeli mają przyczynę osobną i prostszą: dopasowanie po NAZWIE
metody bez jej znaczenia (Files.isRegularFile nie jest Files.exists) oraz
błąd rozpoznania typu odbiornika.
Co to znaczy dla 33%
Trafność 33% zmierzono na innym materiale. Przy klasach JavaFX jest optymistyczna — w tej próbce nie potwierdziło się ani jedno z ośmiu zgłoszeń.
Czego nie sprawdzono
156 z 164 zgłoszeń. Mają ten sam kształt — Stage#, MediaPlayer#,
Region# — więc spodziewać się można tego samego wyniku, ale to podejrzenie,
nie ustalenie. Sprawdzono osiem najwyżej ocenionych, bo to one trafiają do
człowieka pierwsze.
Wykluczenia i wyciszenia
Dwie różne rzeczy, celowo rozdzielone:
exclude— czego nie czytać. Wpływa na populację, więc zmienia też wzorzec: wykluczenie testów potrafi podnieść albo obniżyć konwencję.mute— czego nie pokazywać. Miejsce jest czytane i liczy się do populacji, znika tylko z raportu i rankingu.
Pomylenie ich psuje wynik po cichu: wyciszenie zrobione jako wykluczenie usuwa miejsce z populacji i osłabia regułę, która je złapała.
Plik .odd-one-out.json w badanym katalogu albo --config <ścieżka>:
{
"exclude": ["**/legacy/**", "**/*Generated.java"],
"mute": [
{ "id": "f7946d7a0259", "powod": "setCycleCount to konfiguracja, nie obsługa błędu" }
]
}Domyślna lista wykluczeń (build, target, out, dist, node_modules,
.git, .idea, generated, coverage, *Test.java, *Tests.java,
*IT.java) działa bez konfiguracji; "excludeDefaults": false ją zastępuje
zamiast dokładać. Identyfikatory do mute bierze się z pliku zapisu (--json).
Ranking — co czytać pierwsze
Cztery detektory dają cztery listy w czterech skalach. rank sprowadza je do
jednej liczby:
odd-one-out rank .odd-one-out/java.json .odd-one-out/sql.json --top 20ocena = 100 × konwencja × populacja × rzadkość — iloczyn, nie suma:
zgłoszenie ma być wysoko tylko wtedy, gdy wszystkie trzy składniki są wysokie.
Silna konwencja na trzech przykładach nic nie znaczy, tak samo jak duża
populacja przy połowie miejsc odstających. Suma pozwoliłaby jednemu wysokiemu
składnikowi przykryć zerowy; iloczyn nie.
Skala jest porządkowa. 94 nie znaczy „94% szans, że to błąd", tylko „czytaj
to przed zgłoszeniem o 32".
Stany MIGRACJA W TOKU, ZA MAŁO DANYCH i DO SPRAWDZENIA nie wchodzą do
rankingu w ogóle. Zgłoszenia z tego samego miejsca są scalane w jedną pozycję —
to jedna decyzja dla człowieka — a pozostałe naruszone reguły idą obok jako
uzasadnienie.
Jak czytać wynik
## [2] setOnReady -> setOnError sup=8/10 conf=80% odd=210 miejsc woła setOnReady, 8 z nich woła też setOnError, 2 odstają.
odd=1 przy wysokim sup to najmocniejszy sygnał; odd bliskie połowie
sup znaczy, że konwencji nie ma.
Każde zgłoszenie ma trzy sekcje: co jest niespójne, jak zrobiono w pozostałych miejscach (z przykładem i ścieżką), gotowa poprawka.
Stany, których nie wolno wziąć za zgłoszenie
- ROZJAZD — warstwa jest konwencją, kilka miejsc ją omija. Jedyny stan, który jest zgłoszeniem.
- MIGRACJA W TOKU — obie drogi są liczne. Nie ma czego nazwać odstępstwem; to niedokończone przejście, nie błąd do naprawienia w jednym miejscu.
- ZA MAŁO DANYCH — za mało wystąpień, by mówić o konwencji.
Detektor pom dzieli podobnie: MARTWY (nieobecny w drzewie i nigdzie
niezadeklarowany — dwaj niezależni świadkowie) wobec DO SPRAWDZENIA
(nieobecny w drzewie, ale zadeklarowany — zwykle drzewo z innej rewizji).
Pusty wynik jest poprawnym wynikiem.
Jak to jest testowane
Dziesięć komend, każda odpowiada na inne pytanie. npm test uruchamia cztery
pierwsze i trwa około sześciu sekund.
| komenda | na co odpowiada |
|---|---|
| npm test | wszystko poniżej, co działa po zwykłym sklonowaniu |
| npm run golden | czy pełny przebieg po test/fixtures nadal daje dokładnie zapisany wynik, łącznie z odciskami |
| npm run resilience | czy uszkodzone wejście pada głośno albo przechodzi — nigdy po cichu nie zwraca zera. Jedenaście scenariuszy, każdy uruchamiany dwa razy, uszkodzony i zdrowy, żeby komunikat drukowany także przez zdrowy przebieg nie mógł uchodzić za odpowiedź |
| npm run lang-check | czy w polskim komunikacie został angielski, w angielskim polski, albo czy jakaś proza jest drukowana z pominięciem słownika |
| npm run readme-check | czy to README mówi prawdę — komendy, liczby, odnośniki, przykład wyjścia i zgodność obu wersji, wszystko sprawdzane URUCHOMIENIEM narzędzia, nie czytaniem tekstu |
| npm run amplify | czy wynik naprawdę zależy od wejścia. Każdy wzorzec psuty w sposób o znanym skutku, a liczba, która się nie ruszyła, jest porażką nawet wtedy, gdy narzędzie „zadziałało" |
| npm run known-answers | czy pięć prawdziwych defektów uzasadniających każdy detektor nadal jest znajdowanych, sprawdzane na rewizji sprzed każdej naprawy. Wymaga dwóch prywatnych repozytoriów; przy ich braku daje SKIP z powodem, nigdy zaliczenia |
| npm run foreign | czy zmieniło się to, co narzędzie mówi o netty, JSON-java i spring-petclinic — liczby oraz odciski |
| npm run mutation | czy te testy zauważyłyby, gdyby kod był błędny. Stryker nie jest zależnością; jego brak to SKIP z komendą instalacji |
| npm run self-check | czy narzędzie znajduje coś we własnym źródle |
| npm run full | wszystko razem, z mutacjami — godziny, nie sekundy |
Każda z nich została celowo doprowadzona do porażki co najmniej raz. Bramka, która nigdy nie padła, jest bramką, której nikt nie sprawdził.
Co siedem nieudanych pomiarów robi w README
Sześć pomysłów w tym dokumencie zostało zmierzonych i odrzuconych: reguła timeoutu HTTP (przesłanka nie trzyma się w prawdziwym kodzie), detektor Pythona (brak znanej odpowiedzi), formuła rankingu z JADET (każda pozycja, na której zależało, pogorszyła się), kara za miejsca naruszające wiele reguł (moja własna hipoteza, obalona moimi własnymi liczbami), MUBench (nie do uruchomienia tutaj, a droga częściowa dałaby liczbę brzmiącą jak wynik z benchmarku, nim nie będąc) sygnał wieku (zmierzony, nie pomógł, domyślnie wyłączony) oraz klasy JavaFX (osiem sprawdzonych zgłoszeń, zero defektów — przy API budowniczych główna liczba tego narzędzia jest optymistyczna).
Są tutaj, bo narzędzie publikujące wyłącznie to, co wyszło, nie daje czym ocenić tego, co publikuje — ten sam pomiar, który odrzucił tych sześć, stoi za każdą liczbą, która została.
Liczby, nie przymiotniki
Trafność referencyjna tej klasy narzędzi — PR-Miner (2005): 18,1%. Zmierzone na projekcie autora (114 plików Javy, 21 migracji SQL):
Mierzymy pierwszą piątkę i dziesiątkę, nie całą listę. Trafność całej listy jest myląca, bo nikt całej listy nie czyta — liczy się, ile prawdziwych trafień zobaczysz, zanim przestaniesz czytać.
odd-one-out java <src> --only setOnError na projekcie autora, ustawienia
domyślne — ranking ma 7 pozycji (12 zgłoszeń scalonych po miejscu):
| miara | wynik | |---|---| | prawdziwych w pierwszych 5 | 3 z 5 — 60% | | prawdziwych w pierwszych 10 | 3 z 7 — 43% (lista krótsza niż 10) |
| pozycja | miejsce | ocena |
|---|---|---|
| 1 | Screen.java:1496 | fałszywe |
| 2 | Screen.java:9861 | fałszywe |
| 3 | Menu.java:5753 + :5754 | prawdziwe |
| 4 | Loading.java:397 | prawdziwe (zweryfikowane) |
| 5 | Loading.java:411 | prawdziwe (zweryfikowane) |
| 6 | Screen.java:2121 | fałszywe |
| 7 | Loading.java:974 | fałszywe |
Dwie liczby, nie jedna. Sama trafność narzędziu pochlebia; czas zabiera druga połowa. Obie są tutaj, policzone na tej samej liście:
| | pierwsze 5 | pierwsze 7 (cała scalona lista) | |---|---|---| | trafnych | 3 — 60% | 3 — 43% | | fałszywych alarmów | 2 — 40% | 4 — 57% |
W trybie odkrywania, bez --only, ten sam projekt daje 4 trafne z 14 —
29% trafności, 71% fałszywych alarmów.
Dla skali: komercyjne analizatory statyczne podają wskaźniki fałszywych alarmów od około 1% (SonarQube na OWASP Benchmark; Veracode deklaruje poniżej 1,1% w zastosowaniach firmowych) po 36,3% dla Checkmarksa w raporcie Tolly 2024. To są liczby producentów i benchmarków na syntetycznych zestawach bezpieczeństwa, więc nie porównują się wprost z powyższymi — ale wyznaczają skalę, a to narzędzie stoi na jej niewłaściwym końcu. Tyle w praktyce znaczy „PR-Miner: 18,1%" i taka jest uczciwa cena szukania konwencji zamiast znanych wzorców błędów.
Wszystkie cztery znane odpowiedzi mieszczą się w pierwszej piątce. Nad nimi stoją dokładnie dwa fałszywe alarmy — i to nie jest przypadek, tylko znane ograniczenie opisane niżej.
Pozostałe detektory, każdy na parze ze znaną odpowiedzią:
| detektor | zgłoszeń | prawdziwych |
|---|---|---|
| sql | 1 | 1 — 20260901130000 |
| pom | 1 | 1 — io.thorntail:javafx |
| js | 1 | 1 — closeAiReqLightbox |
| deps | 0 (z 51 przed odsianiem) | brak podstaw do zgłoszenia |
java startował z 3 z 5 dopiero po trzech zmierzonych poprawkach: typ
odbiornika i odsiewanie setterów (obie włączone domyślnie) oraz aliasy,
które wynik pogorszyły i są wyłączone.
Zastrzeżenie do tych 100%: sql, pom i js to wąskie detektory jednoregułowe
na małym, jednorodnym zbiorze. java mieli 11 581 jednostek i tam trafność
siedzi blisko PR-Minerowej. Szum jest oczekiwany i nie jest porażką.
Znane ograniczenie: obsługa o poziom wyżej niż wywołanie
Pozycje 1 i 2 (Screen.java:1496 i :9861) są fałszywe z tej samej
przyczyny: obsługa błędu stoi tam o poziom wyżej niż wywołanie.
bindPlayButtonToPlayerStatus wiąże status przycisku, a błąd obsługuje ten, kto
ją wywołał. Para liczona w zasięgu jednostki tego nie widzi i zgłasza brak.
To nie jest usterka implementacji, tylko granica metody. Ta sama klasa fałszywych
alarmów jest wymieniana przez lintery Fluttera przy sprzątaniu delegowanym do
metody pomocniczej: reguła widzi, że dispose() nie stoi obok utworzenia
kontrolera, choć stoi w metodzie, którą dispose() woła.
Poszerzenie zasięgu do --scope file część takich alarmów usuwa — kosztem
osłabienia samego pojęcia pary (patrz Zasięg pary). Wyciszenie
komentarzem // odd-one-out: ok — obsługa u wołającego załatwia je na stałe
w konkretnym miejscu.
Nisza: tam, gdzie skanery regułowe są bezradne
Obsługa błędów oraz projekt klas i metod to obszary, w których nie ma jednego poprawnego sposobu realizacji — każdy projekt rozstrzyga je po swojemu. Właśnie dlatego skanery regułowe niewiele tam wnoszą: producent narzędzia nie może napisać reguły na coś, co w każdym repozytorium wygląda inaczej.
W obrębie jednego projektu sposób jest jeden, a odstępstwo od niego widoczne. To jest obszar, w którym ten algorytm ma najwięcej do powiedzenia — i powód, dla którego porównuje kod do reszty tego samego repozytorium zamiast do progów z sufitu.
Druga strona problemu: narzędzia generujące uwagi z przeglądu są masowo
ignorowane, gdy nie potrafią uzasadnić zgłoszenia. Uzasadnienie przy każdym
znalezisku (ile razy wzorzec występuje, gdzie, i dlaczego to miejsce odstaje)
oraz jawne rozdzielenie stanów ROZJAZD / MIGRACJA W TOKU /
ZA MAŁO DANYCH istnieją właśnie po to.
Ten akapit celowo nie podaje liczb. Dwie, które tu wcześniej stały — „badanie dwóch tysięcy komentarzy z przeglądów w Javie" i „GitHub: 34% komentarzy AI ignorowanych" — nie dały się potwierdzić u źródła i zostały usunięte. Jedyne liczby w tym dokumencie to te zmierzone niżej, na konkretnym repozytorium, plus 18,1% PR-Minera z publikacji.
Dlaczego nazwy reguł są po polsku
Zgłoszenie niesie identyfikator reguły: sierota, revoke-bez-grant-execute,
martwy-wpis-dependencyManagement, wpis-nieobecny-w-drzewie. Wszystko inne, co
widzi człowiek, zostało przetłumaczone; te nie — i jest to decyzja, a nie
niedoróbka.
Odcisk zgłoszenia liczy się z detektora, reguły, pliku i kotwicy. Przemianowanie reguły zmienia identyfikator każdego zgłoszenia, jakie kiedykolwiek wyprodukowała, co unieważniłoby zapisy wszystkich, którzy narzędzia używają: kolejny przebieg zgłosiłby całą listę jako NOWE, a poprzednią jako ZNIKNĘŁO, i różnica między przebiegami — tryb domyślny, czyli to, po co to narzędzie jest — skłamałaby przez jeden przebieg w sposób nie do odróżnienia od projektu, w którym naprawdę zmieniło się bardzo dużo.
To nie jest rozważanie teoretyczne. Usunięcie jednego bajtu NUL z klucza odcisku zmieniło kiedyś w tym repozytorium każdy identyfikator, podczas gdy wszystkie liczby zostały te same — i właśnie dlatego punkty odniesienia zapisują tutaj odciski, a nie same liczby. Przemianowanie zrobiłoby to samo, tyle że celowo.
Nazwy pojawiają się w wyjściu maszynowym. Nie są pokazywane jako proza: zdanie, które czyta człowiek, idzie przez słownik i jest w jego języku.
Dlaczego tak, a nie inaczej
Dlaczego tree-sitter, a nie wyrażenia regularne (Java). Reguła działa na
parach wywołań na tym samym odbiorniku wewnątrz tej samej funkcji. Regex nie
wie, gdzie kończy się lambda ani co jest odbiornikiem. tree-sitter parsuje ten
projekt czysto: 111 plików, 3 z lokalnym błędem — wszystkie na wzorcach
rekordowych z kwalifikowaną nazwą typu (o instanceof R.Ok(String s)), luka
gramatyki tree-sitter-java 0.23.5. Błąd jest lokalny, parser wraca do siebie
w następnej instrukcji.
Dlaczego NIE tree-sitter dla SQL. GRANT/REVOKE to regularne DDL. Jedyna
realna pułapka to cytowanie dolarowe — ciała plpgsql między $$ ... $$ pełne
średników, na których naiwny podział się rozjeżdża. Dwadzieścia linii tokenizera
rozwiązuje to dokładnie; osobna gramatyka byłaby większym ryzykiem niż zyskiem.
Dlaczego pom wymaga Mavena. dependencyManagement tylko przypina wersje
zależnościom zadeklarowanym gdzie indziej. Wpisu, którego nikt nie deklaruje, nie
da się wykryć z samego pliku — deklaracja bywa tranzytywna albo w profilu. Bez
dependency:tree narzędzie zgadywałoby, więc go wymaga zamiast zgadywać.
Dlaczego progi są takie, a nie inne. minsup 3 — poniżej trzech wystąpień
nie ma populacji, do której można porównywać. minconf 0.6 — niżej „konwencja"
znaczy tyle co rzut monetą. maxodd 3 — gdy odstaje więcej miejsc, to nie
odstępstwo, tylko niedokończona migracja, i narzędzie nazywa to osobno. Wszystkie
są flagami; żaden nie jest wpisany w kod na stałe.
Dlaczego narzędzie nie stosuje poprawek. Narzędzie, które samo poprawia kod, wymaga testów mierzących, czy pomogło. Bez nich pokazanie poprawki jest użyteczne od pierwszego dnia i nic nie ryzykuje.
Dlaczego trzy rozłączne stany zamiast jednej listy. ROZJAZD (warstwa jest
konwencją, kilka miejsc jej nie używa) to jedyny stan będący zgłoszeniem.
MIGRACJA W TOKU (obie drogi liczne) i ZA MAŁO DANYCH są raportowane osobno,
bo przedstawienie ich jako błędu to najprostszy sposób na utratę zaufania
użytkownika. Pusty wynik jest poprawnym wynikiem.
Ograniczenia
- Reguły dla Javy opierają się na nazwach, nie na typach — dwa różne typy o podobnie nazwanych metodach potrafią trafić do jednej reguły.
- To samo miejsce potrafi wyjść w kilku regułach; duplikaty trzeba odsiać.
- Brak wywołania nie dowodzi błędu — sprawdź, czy nie jest robione gdzie indziej.
pomwymaga drzewa z tej samej rewizjipom.xmli tego samego zestawu profili.mvn -P Xwyłącza profileactiveByDefault.- Źródła spoza UTF-8 są po cichu parsowane na śmieci (plik w Latin-2 daje znaki zastępcze i bezsensowne drzewo, bez ostrzeżenia).
- Pamięć:
depstrzyma źródło każdego pliku w pamięci — 547 MB przy 100 tys. linii; ekstrapolowany sufit to kilkaset tysięcy linii. - Dwa równoległe przebiegi piszące do jednego pliku zapisu: wygrywa ten, który pisze później, po cichu. Nie ma blokady, a zapis nie jest niepodzielny.
- Skrypty przeglądarkowe komunikujące się przez
windowsą poza zasięgiemdeps, który stoi na grafie importów (w webowym projekcie autora żaden z 85 plików.jsnie używaimportanirequire).
