İçeriğe geç
academia.sh

Ders 02 / 10

Kod Stili Denetimi

Biçimsiz kodun iki profilde de aynı koştuğunu, yalnız bir stil denetleyicisinin onu bildirdiğini ve biçimlendiricinin anlamı nerede değiştirebileceğini ölçün.

Yayın Güncelleme

Bu derste

  • Biçimsiz kodun iki ayar profilinde de aynı sonucu verdiğini ölçer
  • Belirteçlere bakan küçük bir stil denetleyicisiyle satır sonu boşluğunu, ardışık boşluğu ve karışık girintiyi sayar
  • Bir biçimlendiricinin yalnız boşluk belirteçlerine dokunduğunda anlamı değiştirmediğini gösterir
  • Belirteç tipine bakmayan düzeltmenin dize içeriğini değiştirdiğini ölçer
İçindekiler

Önceki derste koşumun göremediği bir kusuru, kodu çalıştırmadan okuyan bir araç buldu. Kodun biçimi de aynı sınıfa girer mi? Fazladan boşluk, satır sonunda unutulmuş bir sekme ya da yarısı sekme yarısı boşlukla girintilenmiş bir dosya, PHP için hiçbir şey ifade etmeyebilir. Bu ders bu varsayımı ölçer ve ölçü eksenindeki aynı soruyu sorar: bu davranış üretime çıkıyor mu?

Stil denetleyicisi (style checker), kaynağı kod standardına göre okuyup sapmaları bildiren araçtır. Standart önerisi (PSR) kavramı Nesneye Dayalı PHP ve Standartlar kursunda tanıtıldı; kod standardı (coding standard) ise kodun nasıl yazılacağını belirleyen kurallar bütünüdür. Burada standardın içeriği değil, onu uygulayan aracın koşumdaki yeri ölçülür.

Biçimsiz Kod İki Profilde de Koşar

Dersin ölçüm çekirdeği önceki dersle aynıdır. yan.php hiç değişmez; bu derste yalnız kendi yoklamasını yanRaporu() ile basar.

<?php
// yan.php — bu davranış üretime çıkıyor mu?
declare(strict_types=1);

// PHP'nin dağıttığı iki ayar dosyası (php.ini-development, php.ini-production) yedi
// yönergede ayrışır; yedisi buraya birebir alındı. Üretimdeki son iki satır dağıtım
// dosyasında yorum satırıdır: kod önbelleği komut satırında da açık, dosya denetimi kapalı.
const PROFILLER = [
    'geliştirme' => [
        'display_errors' => '1',
        'display_startup_errors' => '1',
        'error_reporting' => '32767',
        'mysqlnd.collect_memory_statistics' => '1',
        'zend.assertions' => '1',
        'zend.exception_ignore_args' => '0',
        'zend.exception_string_param_max_len' => '15',
    ],
    'üretim' => [
        'display_errors' => '0',
        'display_startup_errors' => '0',
        'error_reporting' => '24575',
        'mysqlnd.collect_memory_statistics' => '0',
        'zend.assertions' => '-1',
        'zend.exception_ignore_args' => '1',
        'zend.exception_string_param_max_len' => '0',
        'opcache.enable_cli' => '1',
        'opcache.validate_timestamps' => '0',
    ],
];

/** Betiği ayar dosyası olmadan, yalnız profilin yönergeleriyle ayrı bir süreçte koşturur. */
function calistir(string $betik, array $yonergeler): bool {
    $komut = ['php', '-n'];
    foreach ($yonergeler as $ad => $deger) {
        array_push($komut, '-d', "$ad=$deger");
    }
    $komut[] = $betik;
    $surec = proc_open($komut, [1 => ['pipe', 'w'], 2 => ['file', '/dev/null', 'w']], $borular);
    $cikti = stream_get_contents($borular[1]);
    fclose($borular[1]);
    proc_close($surec);
    $satirlar = array_values(array_filter(array_map('trim', explode("\n", $cikti))));
    return end($satirlar) === 'evet';
}

/** Davranışın iki koşumda görülüp görülmediğinden yanını çıkarır. */
function yan(bool $gelistirmede, bool $uretimde): string {
    return match (true) {
        $gelistirmede && $uretimde => 'iki yanda da',
        $gelistirmede => 'yalnız geliştirmede',
        $uretimde => 'yalnız üretimde',
        default => 'koşuma girmiyor',
    };
}

function yanRaporu(string $ad, string $betik): string {
    $gelistirmede = calistir($betik, PROFILLER['geliştirme']);
    $uretimde = calistir($betik, PROFILLER['üretim']);
    $sonYan = yan($gelistirmede, $uretimde);
    echo "$ad → $sonYan\n    geliştirme ", $gelistirmede ? 'evet' : 'hayır',
        ' · üretim ', $uretimde ? 'evet' : 'hayır', "\n";
    return $sonYan;
}

Ölçülecek kod, bilerek biçimsiz yazılmış küçük bir dosyadır: fonksiyon adından sonra iki boşluk, satır sonunda artık boşluk, bir satırda sekme ve bir satırda dört boşlukla girinti. Dosya sürücü tarafından kaçış dizileriyle üretilir; böylece görünmeyen karakterler dersin kaynağında da görünür kalır.

Küçük Bir Stil Denetleyicisi

Denetleyici önceki dersteki gibi belirteçlere bakar. PHP’nin kaynağı belirteçlere ayırması, boşluğu da bir belirteç yapar: T_WHITESPACE. Boşluk belirteci satır sonlarını ve ardından gelen girintiyi tek parça taşır. Denetleyici bu parçayı satır sonlarında böler: her satır sonundan önce kalan boşluk, satır sonu boşluğudur; satır sonu içermeyen ve en az iki karakterlik boşluk ardışık boşluktur; son satır sonundan sonra kalan boşluk girintidir ve sekme içeriyorsa sekme girintisi, içermiyorsa boşluk girintisi olarak sayılır.

Biçimlendirici (formatter) aynı bölmeyi kullanır, ama sayı çıkarmak yerine boşluğu yeniden yazar: satır sonu boşluğunu siler, ardışık boşluğu tek boşluğa indirir ve her sekmeyi dört boşlukla değiştirir. Boşluk dışındaki her belirteç, metniyle aynen geri yazılır. ciktisi() fonksiyonu, bir dosyayı profilin yönergeleriyle ayrı bir süreçte koşturup çıktısını geri verir.

<?php
// stil.php — belirteçlere bakan stil denetleyicisi ve biçimlendirici
declare(strict_types=1);

/** Boşluk belirteçlerinden dört sayı çıkarır; dize ve yorum içindeki boşluğa bakmaz. */
function stilSayimlari(string $kaynak): array {
    $sayim = ['satirSonu' => 0, 'ardisik' => 0, 'sekmeGirinti' => 0, 'bosluk' => 0];
    foreach (PhpToken::tokenize($kaynak) as $belirtec) {
        if (!$belirtec->is(T_WHITESPACE)) {
            continue;
        }
        $parcalar = explode("\n", $belirtec->text);
        $son = array_pop($parcalar);
        foreach ($parcalar as $parca) {
            if ($parca !== '') {
                $sayim['satirSonu']++;
            }
        }
        if ($parcalar === []) {
            if (strlen($son) >= 2) {
                $sayim['ardisik']++;
            }
        } elseif ($son !== '') {
            $sayim[str_contains($son, "\t") ? 'sekmeGirinti' : 'bosluk']++;
        }
    }
    return $sayim;
}

/** Yalnız boşluk belirteçlerini yeniden yazar; öteki belirteçler aynen kalır. */
function bicimlendir(string $kaynak): string {
    $cikti = '';
    foreach (PhpToken::tokenize($kaynak) as $belirtec) {
        if (!$belirtec->is(T_WHITESPACE)) {
            $cikti .= $belirtec->text;
            continue;
        }
        $parcalar = explode("\n", $belirtec->text);
        $son = array_pop($parcalar);
        if ($parcalar === []) {
            $cikti .= ' ';
            continue;
        }
        $cikti .= str_repeat("\n", count($parcalar)) . str_replace("\t", '    ', $son);
    }
    return $cikti;
}

/** Betiği verilen yönergelerle ayrı bir süreçte koşturur ve çıktısını döndürür. */
function ciktisi(string $betik, array $yonergeler): string {
    $komut = ['php', '-n'];
    foreach ($yonergeler as $ad => $deger) {
        array_push($komut, '-d', "$ad=$deger");
    }
    $komut[] = $betik;
    $surec = proc_open($komut, [1 => ['pipe', 'w'], 2 => ['file', '/dev/null', 'w']], $borular);
    $cikti = stream_get_contents($borular[1]);
    fclose($borular[1]);
    proc_close($surec);
    return $cikti;
}

Sürücü biçimsiz dosyayı üretir, aynı dosyanın biçimlendirilmiş halini de yazar ve üç şeyi ölçer. Birincisi, biçimsiz kodun iki profilde bildirim üretip üretmediği; ikincisi, denetleyicinin ve biçimlendiricinin sayıları; üçüncüsü, dört koşumun çıktısı: biçimsiz ve düzgün dosya, geliştirme ve üretim profili.

<?php
// bicim.php — biçim koşumu değiştirmez
declare(strict_types=1);
require 'yan.php';
require 'stil.php';

$bicimsiz = "<?php\n"
    . "function  topla(\$a,\$b){return \$a+\$b;}   \n"
    . "function  carp(\$a,\$b){\n"
    . "\t\$sonuc = \$a * \$b;\n"
    . "    return \$sonuc;\n"
    . "}\n"
    . "echo topla(2, 3), carp(2, 3), \"\\n\";\n";
file_put_contents('bicimsiz.php', $bicimsiz);
file_put_contents('duzgun.php', bicimlendir($bicimsiz));

file_put_contents('yoklama.php', <<<'PHP'
    <?php
    ob_start();
    include 'bicimsiz.php';
    ob_end_clean();
    echo error_get_last() !== null ? "evet\n" : "hayır\n";
    PHP);
yanRaporu('biçimsiz kod bildirilir', 'yoklama.php');

function sayimSatiri(string $kaynak): string {
    $s = stilSayimlari($kaynak);
    return "satır sonu boşluğu {$s['satirSonu']} · ardışık boşluk {$s['ardisik']}"
        . " · sekme girintisi {$s['sekmeGirinti']} · boşluk girintisi {$s['bosluk']}";
}
echo 'biçimsiz: ', sayimSatiri($bicimsiz), "\n";
echo 'düzgün: ', sayimSatiri(file_get_contents('duzgun.php')), "\n";

$sonuclar = [];
foreach (['bicimsiz.php', 'duzgun.php'] as $dosya) {
    foreach (PROFILLER as $yonergeler) {
        $sonuclar[] = trim(ciktisi($dosya, $yonergeler));
    }
}
echo 'dört koşumun çıktısı: ', implode(' ', $sonuclar), "\n";
echo 'hepsi aynı: ', count(array_unique($sonuclar)) === 1 ? 'evet' : 'hayır', "\n";
$duzgun = file_get_contents('duzgun.php');
echo 'ikinci geçiş dosyayı değiştirir mi: ', bicimlendir($duzgun) === $duzgun ? 'hayır' : 'evet', "\n";
biçimsiz kod bildirilir → koşuma girmiyor
    geliştirme hayır · üretim hayır
biçimsiz: satır sonu boşluğu 1 · ardışık boşluk 2 · sekme girintisi 1 · boşluk girintisi 1
düzgün: satır sonu boşluğu 0 · ardışık boşluk 0 · sekme girintisi 0 · boşluk girintisi 2
dört koşumun çıktısı: 56 56 56 56
hepsi aynı: evet
ikinci geçiş dosyayı değiştirir mi: hayır

Yan, koşuma girmiyor: biçimsiz dosya iki profilde de bildirim üretmeden koşar. Denetleyici ise dört sapma türünü ayrı ayrı sayar; biçimlendirilmiş dosyada satır sonu boşluğu, ardışık boşluk ve sekme girintisi sıfıra iner, boşluk girintisi ikiye çıkar. Dört koşumun çıktısı birebir aynıdır. Biçimlendirici yalnız boşluk belirteçlerine dokunduğu için dosyanın anlamı değişmez; bunu kanıtlayan, dört çıktının eşitliğidir.

Denetim ile Düzeltme Ayrı İşlerdir

Çıktının son satırı iki aracın farkını gösterir. Denetleyici okur ve bildirir; hiçbir dosyayı değiştirmez. Biçimlendirici yazar; bu yüzden aynı dosyaya ikinci kez uygulandığında dosyayı artık değiştirmemesi gerekir, aksi halde her çalıştırma bir sonraki farkı üretir. Ölçüm bunu doğrular: ikinci geçiş dosyayı değiştirmez.

İki aracın ortak noktası, ikisinin de koşumun dışında durmasıdır. İkisinin de üretime gönderilen pakete girmesi gerekmez; geliştiricinin makinesinde ve dağıtımdan önceki adımda koşarlar. Bu, önceki dersin statik çözümleyicisiyle aynı konumdur ve ölçü eksenindeki yanı aynı kılar: koşuma girmiyor. Fark, kusurun sınıfındadır. Statik çözümleme koşumun yakalamadığı bir yanlışı bulur; stil denetimi ise koşumun yanlış saymadığı bir sapmayı bulur. PHP biçimsiz kodu da, satır sonundaki boşluğu da hata saymaz, çünkü ikisi de dilin kurallarına uyar.

Bu yüzden stil denetiminin gerekçesi doğruluk değil, okunurluk ve tutarlılıktır. Satır sonu boşluğu bir sürüm farkında görünmez bir değişiklik olarak belirir; karışık girinti, aynı dosyanın farklı editörlerde farklı hizalanmasına yol açar; ardışık boşluk, bir değişikliğin gerçekte neyi değiştirdiğini gizler. Standardın uygulanmasını insanın dikkatine bırakmak yerine bir araca devretmek, bu üç sapmayı gözden kaçırılabilir olmaktan çıkarır.

Biçimin Koşumu Değiştirdiği Yer

Şimdiye dek biçim koşumu değiştirmedi: boşluk, girinti ve satır sonu boşluğu, çıktıyı bir karakter bile oynatmadı. PHP’de bu kuralın bir istisnası vardır ve istisna, stil denetiminin neden yalnız estetik bir iş sayılmaması gerektiğini gösterir. Dosya <?php ile açılır, ?> ile kapanır; kapanış etiketinden sonra gelen her karakter, PHP kodu değil düz metin olarak çıktıya yazılır. Tek bir istisna vardır: kapanış etiketini hemen izleyen tek satır sonu yutulur.

Ölçüm aynı adlı dosyayı üç ayrı içerikle üretir ve her birini bir çıktı tamponunda dahil eder. Dosyanın yazdığı tek karakter a harfidir; tamponun uzunluğu bundan fazlaysa, kapanış etiketinden sonraki boşluk çıktıya sızmış demektir.

<?php
// kapanis.php — kapanış etiketinden sonraki karakter çıktıya sızar mı
declare(strict_types=1);
require 'yan.php';

$yoklamalar = [
    'tek satır sonu yutulur' => <<<'PHP'
        <?php
        $kapat = '?' . '>';
        file_put_contents('parca.php', "<?php echo 'a'; $kapat\n");
        ob_start();
        include 'parca.php';
        echo strlen(ob_get_clean()) === 1 ? "evet\n" : "hayır\n";
        PHP,
    'ikinci satır sonu sızar' => <<<'PHP'
        <?php
        $kapat = '?' . '>';
        file_put_contents('parca.php', "<?php echo 'a'; $kapat\n\n");
        ob_start();
        include 'parca.php';
        echo strlen(ob_get_clean()) === 2 ? "evet\n" : "hayır\n";
        PHP,
    'kapanıştan sonraki boşluk sızar' => <<<'PHP'
        <?php
        $kapat = '?' . '>';
        file_put_contents('parca.php', "<?php echo 'a'; $kapat ");
        ob_start();
        include 'parca.php';
        echo strlen(ob_get_clean()) === 2 ? "evet\n" : "hayır\n";
        PHP,
];

foreach ($yoklamalar as $ad => $kod) {
    file_put_contents('yoklama.php', $kod);
    yanRaporu($ad, 'yoklama.php');
}
tek satır sonu yutulur → iki yanda da
    geliştirme evet · üretim evet
ikinci satır sonu sızar → iki yanda da
    geliştirme evet · üretim evet
kapanıştan sonraki boşluk sızar → iki yanda da
    geliştirme evet · üretim evet

Üç yoklama da iki yandadır: sızıntı bir ayar farkına bağlı değildir, dilin kuralıdır. Bu yüzden önceki üç sapmadan ayrılır. Satır sonu boşluğu hiçbir şeyi değiştirmezdi; kapanış etiketinden sonraki boşluk ise çıktıyı değiştirir. Gerçek bir uygulamada bu, bir dosyanın sonundaki fazladan bir satırın, o dosyayı dahil eden kodun çıktısına bir karakter eklemesi demektir; çıktısı karakter karakter karşılaştırılan bir sınama ya da ikili bir yanıt, bu yüzden bozulur. Ölçü sınıfı aynıdır: koşuma girmiyor değil, iki yanda da. Saf PHP dosyalarında kapanış etiketini hiç yazmamak, bu sorun sınıfını bütünüyle ortadan kaldırır; PSR-12 gibi stil standartları da saf PHP dosyalarında kapanış etiketinin yazılmamasını kural olarak koyar.

Dosyanın İlk Deyimi

Biçimle ilgili ikinci nokta, declare(strict_types=1) deyiminin yeridir. Bu deyim dosyanın ilk deyimi olmak zorundadır; öncesinde bir echo varsa PHP dosyayı derlemeyi reddeder. Bir stil denetleyicisi bu sırayı bir kural olarak tutabilir, ama sıra yalnız stil değildir; yanlış yerleştirilmiş bir deyim dosyayı çalıştırılamaz kılar. Aşağıdaki sürücü iki küçük dosya yazar, her birini ayrı bir PHP sürecinde koşturur ve çıkış kodunun sıfırdan farklı olup olmadığını sorar.

<?php
// sira.php — declare deyiminin yeri koşumu değiştirir mi
declare(strict_types=1);
require 'yan.php';

$yoklamalar = [
    'declare ilk deyim değilse dosya koşmaz' => <<<'PHP'
        <?php
        file_put_contents('parca.php', "<?php\necho 1;\ndeclare(strict_types=1);\n");
        exec('php -n parca.php 2>/dev/null', $cikti, $kod);
        echo $kod !== 0 ? "evet\n" : "hayır\n";
        PHP,
    'declare ilk deyimse dosya koşar' => <<<'PHP'
        <?php
        file_put_contents('parca.php', "<?php\ndeclare(strict_types=1);\necho 1;\n");
        exec('php -n parca.php 2>/dev/null', $cikti, $kod);
        echo $kod === 0 ? "evet\n" : "hayır\n";
        PHP,
];

foreach ($yoklamalar as $ad => $kod) {
    file_put_contents('yoklama.php', $kod);
    yanRaporu($ad, 'yoklama.php');
}
declare ilk deyim değilse dosya koşmaz → iki yanda da
    geliştirme evet · üretim evet
declare ilk deyimse dosya koşar → iki yanda da
    geliştirme evet · üretim evet

Sonuç yine iki yandadır. Derleme hatası bir ayara değil, dilin söz dizimine bağlıdır; geliştirmede de, üretimde de aynı şekilde oluşur. Geliştirme profilinde hata yanıta basılırken üretimde yalnız günlüğe gider, ama derlenmeyen dosya iki yanda da koşmaz. Bu iki ölçüm birlikte okunduğunda stil denetimi iki sınıfa ayrılır: koşumu hiç etkilemeyen sapmalar (satır sonu boşluğu, girinti, ardışık boşluk) ve koşumu etkileyen sapmalar (kapanış etiketinden sonraki karakter, ilk deyimin yeri). İkinci sınıf, stil denetleyicisinin bulmasının bir kolaylık değil, bir güvence olduğu yerdir.

Sınırlayıcı Ölçüm

Boşluk her zaman boşluk değildir. Bir dize ya da nowdoc içinde satır sonundaki boşluk, dizenin içeriğidir; silinirse program başka bir değer üretir. Bu yüzden belirteç tipine bakmayan bir düzeltme tehlikelidir. Aşağıdaki kaynak, satır sonunda üç boşluk taşıyan bir nowdoc içerir. Aynı dosya üç biçimde koşturulur: özgün haliyle, her satır sonundaki boşluğu silen düzenli ifadeyle düzeltilmiş haliyle ve yalnız T_WHITESPACE belirteçlerine dokunan biçimlendiriciyle.

<?php
// sinir.php — belirteç tipine bakmayan düzeltme anlamı değiştirir
declare(strict_types=1);
require 'yan.php';
require 'stil.php';

$kaynak = "<?php\n"
    . "\$metin = <<<'METIN'\n"
    . "ilk   \n"
    . "ikinci\n"
    . "METIN;\n"
    . "echo strlen(\$metin), \"\\n\";\n";

$surumler = [
    'özgün' => $kaynak,
    'belirteçsiz düzeltme' => preg_replace('/[ \t]+$/m', '', $kaynak),
    'belirteçli biçimlendirme' => bicimlendir($kaynak),
];
foreach ($surumler as $ad => $metin) {
    file_put_contents('metin.php', $metin);
    echo "$ad: ", trim(ciktisi('metin.php', PROFILLER['geliştirme'])), "\n";
}
echo 'belirteçli denetleyicinin saydığı satır sonu boşluğu: ', stilSayimlari($kaynak)['satirSonu'], "\n";
echo 'belirteçsiz düzenli ifadenin bulduğu: ', preg_match_all('/[ \t]+$/m', $kaynak), "\n";
özgün: 13
belirteçsiz düzeltme: 10
belirteçli biçimlendirme: 13
belirteçli denetleyicinin saydığı satır sonu boşluğu: 0
belirteçsiz düzenli ifadenin bulduğu: 1

Özgün dosya ile biçimlendirilmiş dosya aynı uzunlukta (13) bir dize üretir; belirteçsiz düzeltme ise nowdoc içindeki üç boşluğu da silip dizeyi kısaltır. Belirteçli denetleyici nowdoc içeriğini boşluk saymaz, düzenli ifade sayar. Aradaki fark, stil aracının kaynağı metin olarak değil belirteç dizisi olarak okuması gerektiğini gösterir. Biçimlendiricinin güvencesi de buradan gelir: yalnız T_WHITESPACE belirteçlerini yeniden yazan bir araç dize içeriğine erişemez. Sınır ise şudur: bir dize ya da yorum içindeki boşluğun gereksiz olup olmadığı, belirteç okumasının cevaplayabileceği bir soru değildir.

Özet

  • Biçimsiz kod iki profilde de bildirim üretmeden aynı çıktıyla koşar; bu davranışın yanı koşuma girmiyor’dur ve onu yalnız stil denetleyicisi bildirir.
  • Boşluk da bir belirteçtir (T_WHITESPACE): satır sonu boşluğu, ardışık boşluk, sekme girintisi ve boşluk girintisi bu belirteçten sayılır.
  • Biçimlendirici yalnız boşluk belirteçlerini yeniden yazdığında anlamı değiştirmez; biçimsiz ve düzgün dosya dört koşumda aynı çıktıyı verir.
  • Belirteç tipine bakmayan düzeltme, nowdoc ya da dize içindeki satır sonu boşluğunu da siler ve çıktıyı değiştirir.
  • Boşluk biçimi için cevap iki profilde de aynıdır: biçim farkı koşuma girmez.
  • Kapanış etiketinden sonraki karakter çıktıya sızar ve declare(strict_types=1) ilk deyim değilse dosya derlenmez; ikisi de dilin kuralıdır (ölçü sınıfı: iki yanda da).

Sonraki Adım

Bu derste koşumun hiç görmediği ikinci davranış, stil denetleyicisinin elinde bildirime dönüştü. Bir sonraki ders, koşuma giren ama profile göre davranışı değişen bir araca geçer: iddia. Tutmayan bir iddia geliştirmede bir AssertionError atar; aynı iddia üretim profilinde derlenmez bile. Test çerçevelerinin dayandığı bu mekanizmanın hangi yanda durduğunu ve bir testin kendi yardımcısı üzerine kurulduğunda neyin değiştiğini ölçeceğiz.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat