npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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 wiezie bin, src, skills i te dwie strony. ./src/main/java nazywa 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 podac

CO 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 pomoc

Trzy 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 systemie

Goł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ępstwach

Narzę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 ⊃ move działa, ale readFileWithRetry ⊅ readFileSync już 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 na readFileSyncWithRetry.

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 wydaniu

Szukany 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 lista

Gdy 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 setOnError usunęło jedno zgłoszenie i utworzyło trzy — bo to miejsce ma teraz setOnError, ale nie ma setCycleCount, setOnHalted ani setOnEndOfMedia. 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):

  1. obie strony to zwykłe settery — set[A-Z], ale nie setOn[A-Z];
  2. odbiornik powstał w tej samej jednostce (new X() obok);
  3. 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#initOwner

Zweryfikowane 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 lambda daje 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.

  1. 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.
  2. Nie ma tu czym go zbudować. java nie jest na PATH i mvn nie jest na PATH.
  3. 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 obraz svamann/mubench:stable jest 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 20

ocena = 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=2

10 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.
  • pom wymaga drzewa z tej samej rewizji pom.xml i tego samego zestawu profili. mvn -P X wyłącza profile activeByDefault.
  • Ź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ęć: deps trzyma ź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 window są poza zasięgiem deps, który stoi na grafie importów (w webowym projekcie autora żaden z 85 plików .js nie używa import ani require).