Go Dilinde İlk Linter’ınızı Nasıl Oluşturursunuz?
Yazılım geliştirme sürecinde kod kalitesi, tutarlılık ve hata tespiti her zaman öncelikli konular olmuştur. Peki, kodunuzdaki potansiyel sorunları daha derleyiciye gitmeden, hatta kod incelemesine (code review) bile gelmeden otomatik olarak yakalamak istemez misiniz? İşte tam da bu noktada linter’lar devreye giriyor. Go dilinin sunduğu güçlü statik analiz araçları ve esnek yapısı sayesinde, kendi özel linter’ınızı oluşturmak sandığınızdan çok daha kolay. Bu makalede, Go dilinde sıfırdan bir linter geliştirmek için adım adım bir yolculuğa çıkacak, temel kavramlardan ileri düzey ipuçlarına kadar her şeyi keşfedeceğiz. Böylece, ekip standartlarınıza uygun, daha temiz ve hatasız kod yazma alışkanlığı kazanmanıza yardımcı olacak güçlü bir araca sahip olacaksınız.
Neden Bir Linter’a İhtiyacımız Var? Kod Kalitesini Otomatikleştirme Sanatı
Her geliştirici, kariyerinin bir noktasında kötü yazılmış, okunması zor veya gizli hatalar barındıran kodlarla karşılaşmıştır. Bu tür kodlar, projenin bakımını zorlaştırır, yeni özellik eklemeyi yavaşlatır ve uzun vadede maliyetleri artırır. Manuel kod incelemeleri (code review) bu sorunları tespit etmede etkili olsa da, insan hatasına açık ve zaman alıcı bir süreçtir. Ayrıca, her ekip üyesinin aynı kodlama standartlarına uymasını sağlamak da sürekli bir çaba gerektirir. İşte bu noktada linter’lar, geliştirme sürecinin vazgeçilmez bir parçası haline gelir.
Bir linter (genellikle “lint” olarak da bilinir), kaynak kodunuzu statik olarak analiz eden bir araçtır. Yani, kodu çalıştırmadan önce, olası hataları, stil ihlallerini, şüpheli yapıları veya belirli kurallara uymayan durumları tespit eder. Go ekosistemi, bu konuda oldukça zengin bir yapıya sahiptir. Go’nun kendisi, go vet gibi temel statik analiz araçlarıyla gelir ve topluluk tarafından geliştirilen birçok güçlü linter (örneğin golangci-lint) mevcuttur. Ancak bazen, standart linter’ların kapsamadığı, projenize veya ekibinize özel kurallara ihtiyacınız olabilir. Örneğin, belirli bir kütüphanenin kullanımını kısıtlamak, belirli bir fonksiyon adlandırma kuralını zorunlu kılmak veya “magic number” (doğrudan kod içine yazılmış, anlamı belirsiz sabit sayılar) kullanımını engellemek isteyebilirsiniz. Kendi linter’ınızı oluşturmak, size bu özel ihtiyaçları karşılama esnekliği sunar.
Kendi linter’ınızı geliştirmenin faydaları saymakla bitmez: Öncelikle, kod tabanınızda tutarlılığı artırırsınız. Herkes aynı kurallara uyduğu için, farklı geliştiriciler tarafından yazılmış kodlar bile benzer bir yapıya sahip olur. İkinci olarak, potansiyel hataları erken aşamada yakalayarak geliştirme döngüsünü hızlandırırsınız. Derleme zamanı hatalarından önce linter hatalarını düzeltmek, çok daha az maliyetlidir. Üçüncü olarak, kod inceleme süreçlerini daha verimli hale getirirsiniz. Linter’ın otomatik olarak bulduğu sorunlarla uğraşmak yerine, inceleyiciler daha çok mimari kararlara ve iş mantığına odaklanabilirler. Son olarak, yeni ekip üyelerinin projenin kodlama standartlarına daha hızlı adapte olmasına yardımcı olursunuz. Linter, bir nevi otomatik bir mentor görevi görerek doğru pratikleri teşvik eder. Go’nun güçlü go/ast (Abstract Syntax Tree – Soyut Sözdizimi Ağacı) ve golang.org/x/tools/go/analysis paketleri sayesinde, bu süreci oldukça kolay ve keyifli bir hale getirebiliriz.
Linter Nedir ve Go’da Nasıl Çalışır? Soyut Sözdizimi Ağaçlarına Giriş
Bir linter’ın kalbinde, kaynak kodunuzu anlama yeteneği yatar. Bilgisayarlar, yazdığımız düz metin kodu doğrudan yorumlayamazlar. Bunun yerine, bu kodu bir dizi aşamadan geçirerek anlamlı bir yapıya dönüştürürler. Bu aşamalardan biri de “parsing” (ayrıştırma) işlemidir. Ayrıştırma sonucunda, kodun yapısal temsilini gösteren bir Soyut Sözdizimi Ağacı (Abstract Syntax Tree – AST) elde edilir. AST, programın hiyerarşik yapısını düğümler ve dallar aracılığıyla gösteren bir veri yapısıdır. Her bir düğüm, kodun bir bölümünü (örneğin, bir değişken bildirimi, bir fonksiyon çağrısı, bir if ifadesi veya bir operatör) temsil eder.
Go dilinde, AST ile çalışmak için oldukça güçlü yerleşik paketler bulunur. go/parser paketi, Go kaynak kodunu ayrıştırarak bir AST oluşturmanızı sağlar. go/ast paketi ise, bu AST’yi temsil eden veri yapılarını ve ağaç üzerinde gezinmek için yardımcı fonksiyonları içerir. Linter’lar tam da bu AST üzerinde çalışır. Bir linter, kaynak kodunu bir AST’ye dönüştürdükten sonra, bu ağacı dolaşır ve belirli desenleri, yapıları veya kuralları arar. Örneğin, bir linter, bir fonksiyonun çok uzun olup olmadığını, belirli bir değişkenin kullanılmadığını veya bir “import” ifadesinin yanlış yerde olup olmadığını kontrol etmek için AST’yi kullanabilir.
Go’nun go/analysis framework’ü (golang.org/x/tools/go/analysis paketi altında bulunur), linter yazmayı standartlaştıran ve kolaylaştıran güçlü bir yapıdır. Bu framework, bir linter’ı bir analysis.Analyzer olarak tanımlamanızı sağlar. Bir Analyzer, kendi başına bir analiz birimi olup, belirli bir kontrolü veya kuralı uygular. Bu framework, farklı linter’ların bir araya getirilerek tek bir araç altında çalıştırılmasına olanak tanır, tıpkı golangci-lint‘in yaptığı gibi. Her bir Analyzer, Run adında bir metot içerir. Bu metot, kendisine bir analysis.Pass nesnesi alarak çalışır. analysis.Pass nesnesi, analiz edilen paketin AST’si, dosya kümeleri, tip bilgileri ve diğer bağlam bilgilerini içerir. Linter’ınız, bu Pass nesnesini kullanarak AST üzerinde gezinebilir ve bulduğu sorunları pass.Reportf fonksiyonu aracılığıyla raporlayabilir.
Kısacası, Go’da bir linter oluşturmak için şu adımları izleyeceğiz: Öncelikle, go/parser kullanarak kaynak kodu bir AST’ye dönüştüreceğiz. Ardından, go/ast paketindeki ast.Inspect gibi fonksiyonları kullanarak bu AST üzerinde gezineceğiz. Son olarak, golang.org/x/tools/go/analysis framework’ünü kullanarak kendi analizörümüzü tanımlayacak ve bulduğumuz sorunları bu framework’ün raporlama mekanizmasıyla kullanıcılara sunacağız. Bu yapı, hem basit hem de karmaşık linter kuralları geliştirmek için oldukça esnek ve güçlü bir temel sağlar.
Temel Bir Go Linter’ı Oluşturmaya Başlangıç: Proje Yapısı ve İlk Adımlar
Kendi Go linter’ımızı oluşturmaya başlamadan önce, basit bir proje yapısı kuralım ve gerekli Go modüllerini tanımlayalım. Bu, projemizi düzenli tutmamıza ve bağımlılıkları yönetmemize yardımcı olacaktır. Linter’ımızı, cmd dizini altında çalıştırılabilir bir uygulama olarak, ana analiz mantığını ise ayrı bir pakette tutacak şekilde tasarlayacağız. Bu ayrım, linter’ımızı hem bağımsız bir araç olarak çalıştırmamıza hem de başka araçlar (örneğin golangci-lint) tarafından bir kütüphane olarak kullanılmasına olanak tanır.
İlk olarak, projemiz için bir dizin oluşturalım ve Go modülünü başlatalım:
mkdir mylinter
cd mylinter
go mod init github.com/kullaniciadi/mylinter
Şimdi, linter’ımızın ana mantığını barındıracak paketi ve çalıştırılabilir dosyasını oluşturalım:
mkdir internal/mylinter
mkdir cmd/mylinter
touch internal/mylinter/mylinter.go
touch cmd/mylinter/main.go
Proje yapımız şu şekilde görünecektir:
mylinter/
├── cmd/
│ └── mylinter/
│ └── main.go
└── internal/
└── mylinter/
└── mylinter.go
Şimdi, internal/mylinter/mylinter.go dosyamızda linter’ımızın çekirdeğini tanımlayalım. Bu dosya, golang.org/x/tools/go/analysis framework’ünden analysis.Analyzer yapısını kullanarak linter’ımızı tanımlayacak. İlk linter kuralımız olarak, çok basit bir şey yapalım: belirli bir paketin (örneğin, “fmt” paketi) doğrudan kullanımını engellemeye çalışalım. Bu, genellikle bir projenin kendi loglama veya hata yönetimi kütüphanelerini kullanmasını teşvik etmek için yapılır.
// internal/mylinter/mylinter.go
package mylinter
import (
"go/ast"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
// Analyzer, linter'ımızın ana tanımıdır.
var Analyzer = &analysis.Analyzer{
Name: "noFmtImport", // Linter'ımızın adı
Doc: "fmt paketinin doğrudan kullanımını engeller.", // Linter'ımızın açıklaması
Run: run, // Linter'ın çalıştırılacak fonksiyonu
Requires: []*analysis.Analyzer{
inspect.Analyzer, // AST üzerinde gezinmek için gerekli olan analizör
},
}
func run(pass *analysis.Pass) (interface{}, error) {
// inspect.Analyzer tarafından sağlanan AST inspector'ı alalım.
// Bu, AST üzerinde kolayca gezinmemizi sağlar.
inspector := pass.ResultOf[inspect.Analyzer].(*ast.Inspector)
// Sadece dosya seviyesindeki import bildirimlerini kontrol etmek için filtre uygulayalım.
nodeFilter := []ast.Node{
(*ast.ImportSpec)(nil),
}
inspector.Preorder(nodeFilter, func(n ast.Node) {
importSpec := n.(*ast.ImportSpec)
// Import yolunu tırnak işaretlerinden arındırarak alalım.
importPath := importSpec.Path.Value
if importPath == "fmt" {
// Eğer "fmt" paketi import edilmişse, bir hata raporlayalım.
pass.Reportf(importSpec.Pos(), "fmt paketi kullanılamaz, lütfen özel loglama paketinizi kullanın.")
}
})
return nil, nil
}
Yukarıdaki kodda, analysis.Analyzer yapısını tanımladık. Name ve Doc alanları linter’ımızı tanıtır. Run alanı, linter’ın ana mantığını içeren fonksiyondur. Requires alanı ise, bu linter’ın çalışabilmesi için başka hangi analizörlere ihtiyaç duyduğunu belirtir. Burada inspect.Analyzer‘ı kullandık, çünkü bu bize AST üzerinde kolayca gezinmemizi sağlayan bir ast.Inspector nesnesi sunar. run fonksiyonu içinde, ast.Inspector‘ı kullanarak AST’yi dolaşıyor ve her ast.ImportSpec (import bildirimi) düğümünü kontrol ediyoruz. Eğer import yolu "fmt" ise, pass.Reportf ile bir hata mesajı oluşturuyoruz. Bu, Go’da bir linter’ın temel çalışma prensibidir.
Go Analyzer Framework’ü ile Tanışma: Detaylı Bir Bakış
Go’nun linter ekosisteminin temelini oluşturan golang.org/x/tools/go/analysis framework’ü, statik analiz araçları geliştirmek için standart ve güçlü bir yol sunar. Bu framework, farklı analizörlerin bir araya getirilerek karmaşık kontrollerin yapılmasını kolaylaştırır ve linter’ların test edilebilirliğini artırır. Bir analysis.Analyzer yapısı, bir linter’ın tüm kimliğini ve davranışını tanımlayan merkezi bir bileşendir. Hadi bu yapının temel alanlarına daha yakından bakalım:
-
Name string: Analizörün benzersiz adıdır. Genellikle küçük harfle başlar ve kısa, açıklayıcı bir isimdir (örneğin, “nolint”, “unusedparams”). -
Doc string: Analizörün ne iş yaptığını açıklayan kısa bir belgedir. Bu açıklama, kullanıcıların linter’ın amacını anlamalarına yardımcı olur. -
Run func(*analysis.Pass) (interface{}, error): Analizörün ana mantığını içeren fonksiyondur. Bu fonksiyon, biranalysis.Passnesnesi alır.Passnesnesi, analiz edilen paketin AST’si, tip bilgileri, dosya sistemindeki konumu ve diğer bağlam bilgilerini içerir.Runfonksiyonu, bulduğu sorunlarıpass.Reportfveyapass.Reportmetotları aracılığıyla raporlar. Dönüş değeri olaraknil, nilyaygın bir kullanımdır, ancak bazı analizörler ileri analizler için sonuç döndürebilir. -
Requires []*analysis.Analyzer: Bu analizörün çalışabilmesi için önceden çalıştırılması gereken diğer analizörlerin listesidir. Örneğin, AST üzerinde gezmek içininspect.Analyzer‘a, tip bilgileri içintypes.Analyzer‘a ihtiyaç duyabilirsiniz. Bu, analizörlerin modüler bir şekilde birbirine bağımlı olmasını sağlar. -
FactTypes []analysis.Fact: Analizörün ürettiği veya tükettiği “fact” (gerçek) türlerini tanımlar. Fact’ler, analizörler arasında bilgi paylaşımını sağlayan mekanizmalardır. Örneğin, bir analizör bir fonksiyonun yan etkileri hakkında bir fact üretebilir ve başka bir analizör bu fact’i kullanarak farklı bir kontrol yapabilir. -
ResultType reflect.Type:Runmetodunun döndürdüğü sonucun tipini belirtir. EğerRunmetodu bir değer döndürüyorsa, bu alan o değerin tipini yansıtmalıdır.
analysis.Pass yapısı, bir analizörün Run fonksiyonuna sağlanan ana bağlam nesnesidir. Bu nesne, bir analizörün ihtiyaç duyduğu tüm bilgilere erişim sağlar:
-
Fset *token.FileSet: Kaynak dosyalardaki konumları (satır, sütun) izlemek için kullanılan dosya kümesi. -
Files []*ast.File: Analiz edilen paketteki her bir Go kaynak dosyasının AST’si. -
Pkg *types.Package: Analiz edilen paketin tip bilgileri. -
TypesInfo *types.Info: AST düğümleri ve bunların tipleri, tanımları, kullanımları arasındaki eşleşmeleri içeren detaylı tip bilgileri. -
Reportf func(pos token.Pos, format string, args ...interface{}): Bir hata veya uyarı raporlamak için kullanılan fonksiyondur.posparametresi, hatanın kaynak kodundaki konumunu belirtir. -
ResultOf map[*analysis.Analyzer]interface{}:Requiresalanında belirtilen diğer analizörlerin döndürdüğü sonuçlara erişim sağlar. Örneğin,inspect.Analyzer‘ın sonucuna buradan erişerek birast.Inspectornesnesi elde ederiz.
AST üzerinde gezinmek için en yaygın yöntemlerden biri ast.Inspect fonksiyonudur. Bu fonksiyon, bir AST düğümünü ve bir gezgin (visitor) fonksiyonu alır. Gezgin fonksiyonu, ağaçtaki her düğüm için çağrılır. Bu sayede, ağacın tamamını veya belirli düğüm türlerini kolayca kontrol edebiliriz. Örneğin, bir fonksiyon bildirimini (ast.FuncDecl), bir if ifadesini (ast.IfStmt) veya bir literal değeri (ast.BasicLit) arayabiliriz. Bu framework’ün sağladığı modüler yapı ve kapsamlı bilgiler sayesinde, Go kod tabanınız için çok spesifik ve güçlü statik analiz araçları geliştirebilirsiniz.
Pratik Bir Linter Kuralı Geliştirme: “Magic Number” Kontrolü
Şimdiye kadar temel linter yapısını ve Go Analyzer framework’ünü öğrendik. Artık daha pratik ve yaygın bir kodlama sorununu ele alan bir linter kuralı geliştirelim: “Magic Number” (sihirli sayı) kontrolü. “Magic number” terimi, kod içinde doğrudan kullanılan, ancak anlamı belirsiz olan sayısal sabitleri ifade eder. Örneğin, bir dosya boyutunu kontrol etmek için if size > 1024 * 1024 yazmak yerine, const MB = 1024 * 1024 tanımlayıp if size > MB yazmak çok daha okunabilir ve sürdürülebilirdir. Magic number’lar, kodun anlaşılırlığını azaltır, hata yapma olasılığını artırır ve bakımını zorlaştırır. Linter’ımız, doğrudan kullanılan sayısal sabitleri tespit ederek geliştiricileri bu sayıları adlandırılmış sabitlere dönüştürmeye teşvik edecek.
Bu linter kuralını internal/mylinter/mylinter.go dosyamıza ekleyelim veya mevcut Analyzer yapımızı güncelleyelim. Amacımız, AST’yi gezerken ast.BasicLit (temel değişmez) düğümlerini bulmak ve bunların bir sayısal literal olup olmadığını kontrol etmektir. Eğer bir sayısal literal ise ve bir const bildiriminin parçası değilse, bunu bir magic number olarak raporlayabiliriz. Ancak, daha basit bir başlangıç için, şimdilik sadece doğrudan kullanılan sayısal literal’ları hedef alalım ve belirli istisnalar (örneğin 0, 1 gibi çok yaygın sayılar) dışında hepsini raporlayalım.
// internal/mylinter/mylinter.go (Güncellenmiş veya yeni Analyzer)
package mylinter
import (
"go/ast"
"go/token"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"strconv"
)
// Analyzer, linter'ımızın ana tanımıdır.
var Analyzer = &analysis.Analyzer{
Name: "noMagicNumber",
Doc: "Doğrudan kullanılan 'magic number'ları tespit eder ve sabit kullanımını teşvik eder.",
Run: run,
Requires: []*analysis.Analyzer{
inspect.Analyzer,
},
}
func run(pass *analysis.Pass) (interface{}, error) {
inspector := pass.ResultOf[inspect.Analyzer].(*ast.Inspector)
// Sadece sayısal literal'ları (örneğin 10, 3.14) kontrol etmek için filtre uygulayalım.
nodeFilter := []ast.Node{
(*ast.BasicLit)(nil),
}
inspector.Preorder(nodeFilter, func(n ast.Node) {
lit := n.(*ast.BasicLit)
// Sadece sayısal literal'ları (INT, FLOAT) kontrol edelim.
if lit.Kind != token.INT && lit.Kind != token.FLOAT {
return
}
// Bazı yaygın ve genellikle kabul edilebilir sayıları (0, 1, -1) görmezden gelelim.
// Bu listeyi projenizin ihtiyaçlarına göre genişletebilirsiniz.
if lit.Value == "0" || lit.Value == "1" || lit.Value == "-1" {
return
}
// Sayının bir 'const' bildiriminin parçası olup olmadığını kontrol edelim.
// Bu, biraz daha karmaşık bir kontrol gerektirir.
// Şimdilik, sadece literal'ın bir 'const' ifadesi içinde olup olmadığını kontrol etmek için
// parent düğümlerine bakabiliriz. Daha doğru bir kontrol için 'types.Info' gerekebilir.
// Basit bir yaklaşımla: Eğer literal bir 'const' bildirimi içinde değilse, raporla.
// Bu kontrol tam doğru olmayabilir, çünkü bir ifadenin parçası olan const'lar da olabilir.
// Daha sağlam bir kontrol için AST bağlamını daha derinlemesine incelemek gerekir.
// Şimdilik, bu basit kontrolü kullanarak ilerleyelim.
// Bir sayısal literal'ı int veya float olarak ayrıştırmaya çalışalım.
// Geçerli bir sayısal değer olduğundan emin olalım.
_, errInt := strconv.ParseInt(lit.Value, 0, 64)
_, errFloat := strconv.ParseFloat(lit.Value, 64)
if errInt == nil || errFloat == nil { // Geçerli bir sayısal değerse
// Eğer literal bir 'const' bildirimi içinde değilse raporlayalım.
// Bu kontrolü basitleştirmek adına, şimdilik her sayısal literal'ı magic number olarak kabul edelim
// ve sadece "0", "1", "-1" gibi istisnaları hariç tutalım.
// Gerçek bir linter'da, bu kontrolün daha sofistike olması gerekir (örneğin, bir atama ifadesinin sağ tarafında mı, bir const bildirimi içinde mi vb.).
pass.Reportf(lit.Pos(), "magic number '%s' kullanılıyor; lütfen adlandırılmış bir sabit kullanın", lit.Value)
}
})
return nil, nil
}
Yukarıdaki kodda, ast.BasicLit türündeki düğümleri hedef alıyoruz. lit.Kind ile literal’ın türünü kontrol ediyoruz (token.INT veya token.FLOAT). Ardından, “0”, “1”, “-1” gibi yaygın ve genellikle kabul edilebilir sayıları atlıyoruz. Eğer bu koşullar sağlanırsa, pass.Reportf ile bir hata raporluyoruz. Bu, linter’ımızın magic number’ları tespit etmesini sağlar. Daha gelişmiş bir linter’da, bir literal’ın bir const bildiriminin parçası olup olmadığını veya bir iota ifadesi içinde kullanılıp kullanılmadığını kontrol etmek için AST bağlamını daha derinlemesine incelemeniz gerekebilir. Ancak bu örnek, temel bir kuralı nasıl uygulayacağınızı göstermek için yeterlidir.
Linter’ınızı Test Etme ve Entegre Etme
Linter’ımızı geliştirdikten sonra, doğru çalıştığından emin olmak için test etmemiz ve geliştirme iş akışımıza entegre etmemiz gerekir. Go’nun go/analysis/analysistest paketi, analizörleri test etmek için özel olarak tasarlanmış güçlü bir araçtır. Bu paket, test senaryolarınızı kolayca tanımlamanıza ve analizörünüzün beklenen hataları doğru bir şekilde raporlayıp raporlamadığını doğrulamanıza olanak tanır.
Linter’ı Yerel Olarak Çalıştırma
Linter’ımızı yerel olarak çalıştırmak için, cmd/mylinter/main.go dosyamızı aşağıdaki gibi düzenlememiz gerekiyor. Bu dosya, singlechecker paketini kullanarak bizim Analyzer‘ımızı çalıştırır.
// cmd/mylinter/main.go
package main
import (
"github.com/kullaniciadi/mylinter/internal/mylinter"
"golang.org/x/tools/go/analysis/singlechecker"
)
func main() {
singlechecker.Main(mylinter.Analyzer)
}
Şimdi, linter’ımızı test etmek için örnek bir Go dosyası oluşturalım: testdata/test.go.
// testdata/test.go
package testdata
import "fmt" // Bu satır fmt paketi kullanımını raporlamalı
const (
MyConst = 100 // Bu bir magic number değil
AnotherConst = 2
)
func DoSomething(value int) int {
result := value * 5 // 5 bir magic number, raporlanmalı
if result > 10 { // 10 bir magic number, raporlanmalı
fmt.Println("Result is greater than", 10) // Hem fmt hem de 10 raporlanmalı
return result + 3 // 3 bir magic number, raporlanmalı
}
return result
}
func IgnoreZeroOne() {
_ = 0 // Raporlanmamalı
_ = 1 // Raporlanmamalı
_ = -1 // Raporlanmamalı
}
Linter’ımızı çalıştırmak için: (Eğer noFmtImport linter’ını kullanıyorsanız, internal/mylinter/mylinter.go dosyasındaki Analyzer değişkenini noFmtImportAnalyzer veya benzeri bir isimle değiştirmeniz ve singlechecker.Main içinde doğru Analyzer‘ı çağırmanız gerekebilir. Bu örnekte noMagicNumber analizörünü varsayıyoruz.)
go run ./cmd/mylinter ./testdata/...
Bu komut, linter’ınızı çalıştıracak ve testdata dizinindeki Go dosyalarını analiz edecektir. Beklentimiz, fmt paketi import edildiğinde ve magic number’lar kullanıldığında hata mesajları görmektir.
Test Yazma (analysistest ile)
Daha sağlam bir test yaklaşımı için analysistest paketini kullanalım. internal/mylinter/mylinter_test.go dosyasını oluşturalım:
// internal/mylinter/mylinter_test.go
package mylinter_test
import (
"testing"
"github.com/kullaniciadi/mylinter/internal/mylinter"
"golang.org/x/tools/go/analysis/analysistest"
)
func TestNoMagicNumber(t *testing.T) {
// Test verilerinin bulunduğu dizin.
// Bu dizin, test için Go kaynak dosyalarını içerecektir.
testdata := analysistest.TestData()
// Analizörümüzü ve test veri kümemizi çalıştıralım.
// analysistest.Run fonksiyonu, analizörün belirli test senaryoları üzerinde
// nasıl davrandığını kontrol etmemizi sağlar.
// analysistest.Run(t, testdata, mylinter.Analyzer, "a")
// "a" paketi, testdata dizinindeki 'a' adlı paketi analiz etmesini söyler.
// Bu durumda, testdata dizininde 'a' adlı bir paket oluşturmamız gerekecek.
analysistest.Run(t, testdata, mylinter.Analyzer, "a")
}
Şimdi, test verilerimizi testdata/src/a dizini altına yerleştirelim. analysistest paketi, // want "hata mesajı" yorumlarını kullanarak beklenen hataları otomatik olarak kontrol eder.
// testdata/src/a/a.go
package a
import "fmt" // want "fmt paketi kullanılamaz, lütfen özel loglama paketinizi kullanın."
const (
MyConst = 100
AnotherConst = 2
)
func DoSomething(value int) int {
result := value * 5 // want "magic number '5' kullanılıyor; lütfen adlandırılmış bir sabit kullanın"
if result > 10 { // want "magic number '10' kullanılıyor; lütfen adlandırılmış bir sabit kullanın"
fmt.Println("Result is greater than", 10) // want "fmt paketi kullanılamaz, lütfen özel loglama paketinizi kullanın." // want "magic number '10' kullanılıyor; lütfen adlandırılmış bir sabit kullanın"
return result + 3 // want "magic number '3' kullanılıyor; lütfen adlandırılmış bir sabit kullanın"
}
return result
}
func IgnoreZeroOne() {
_ = 0
_ = 1
_ = -1
}
Şimdi testleri çalıştırın:
go test ./internal/mylinter
Bu, linter’ınızın beklenen hataları doğru bir şekilde yakalayıp yakalamadığını doğrulayacaktır. analysistest paketi, // want yorumlarını tarayarak, linter’ın bu konumlarda belirtilen mesajlarla hata raporlamasını bekler.
CI/CD Süreçlerine Entegrasyon
Linter’ınızı geliştirme iş akışınıza entegre etmek, kod kalitesini sürekli olarak yüksek tutmanın anahtarıdır. Bunu genellikle CI/CD (Sürekli Entegrasyon/Sürekli Teslimat) boru hatlarında yaparsınız. Örneğin, GitHub Actions, GitLab CI/CD veya Jenkins gibi araçlarda, her kod gönderimi (push) veya çekme isteği (pull request) üzerinde linter’ınızı otomatik olarak çalıştırabilirsiniz.
Örnek bir GitHub Actions yapılandırması (.github/workflows/lint.yml):
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Run my custom linter
run: |
go run ./cmd/mylinter ./...
Bu yapılandırma, her push ve pull request’te Go projenizi kontrol edecek ve özel linter’ınızı tüm kod tabanı üzerinde çalıştıracaktır. Eğer linter herhangi bir hata raporlarsa, CI/CD adımı başarısız olacak ve bu da geliştiricilerin sorunları birleştirmeden önce düzeltmelerini sağlayacaktır. Bu sayede, kod tabanınızın sürekli olarak yüksek kalitede kalmasını garantilemiş olursunuz.
İleri Düzey Linter Geliştirme İpuçları
Temel bir linter oluşturmayı ve test etmeyi öğrendik. Şimdi, linter’ınızı daha güçlü ve esnek hale getirecek bazı ileri düzey ipuçlarına göz atalım. Bu ipuçları, daha karmaşık senaryoları ele almanıza ve linter’ınızı projenizin özel ihtiyaçlarına göre daha iyi uyarlamanıza yardımcı olacaktır.
1. Konfigürasyon Seçenekleri Ekleme
Bir linter’ın her zaman aynı kuralları uygulaması yerine, kullanıcıların belirli parametreleri yapılandırmasına izin vermek çok daha kullanışlıdır. Örneğin, “magic number” linter’ımızda, hangi sayıların göz ardı edileceğini veya hangi eşik değerlerinin kullanılacağını yapılandırılabilir hale getirebiliriz. analysis.Analyzer yapısı, Flags adında bir alan içerir. Bu alan, standart Go flag paketiyle uyumlu çalışır ve linter’ınıza komut satırı argümanları aracılığıyla konfigürasyon eklemenizi sağlar.
// internal/mylinter/mylinter.go (Örnek konfigürasyon ekleme)
// ...
var (
ignoreZeros bool
minMagicNumber int
)
var Analyzer = &analysis.Analyzer{
Name: "noMagicNumber",
Doc: "Doğrudan kullanılan 'magic number'ları tespit eder ve sabit kullanımını teşvik eder.",
Run: run,
Requires: []*analysis.Analyzer{
inspect.Analyzer,
},
// Flags alanını kullanarak komut satırı argümanları ekleyelim.
Flags: func() flag.FlagSet {
fs := flag.NewFlagSet("noMagicNumber", flag.ExitOnError)
fs.BoolVar(&ignoreZeros, "ignore-zeros", true, "0 ve 1 sayılarını magic number olarak görmezden gel.")
fs.IntVar(&minMagicNumber, "min-magic-number", 2, "Bu değerin altındaki sayıları magic number olarak görmezden gel.")
return *fs
}(),
}
func run(pass *analysis.Pass) (interface{}, error) {
// ...
if ignoreZeros && (lit.Value == "0" || lit.Value == "1" || lit.Value == "-1") {
return
}
// minMagicNumber kontrolünü de ekleyebilirsiniz.
// ...
}
Bu sayede, linter’ınızı go run ./cmd/mylinter -ignore-zeros=false -min-magic-number=5 ./... gibi komutlarla çalıştırarak davranışını değiştirebilirsiniz.
2. Birden Fazla Kural Yönetimi ve Kural Grupları
Gerçek dünya linter’ları genellikle birden fazla kural içerir. Her kuralı ayrı bir analysis.Analyzer olarak tanımlamak ve sonra bunları tek bir ana linter aracında birleştirmek iyi bir uygulamadır. singlechecker.Main yerine, birden fazla analizörü çalıştırmak için multichecker.Main kullanabilirsiniz.
// cmd/mylinter/main.go (Birden fazla analizör için)
package main
import (
"github.com/kullaniciadi/mylinter/internal/mylinter"
"github.com/kullaniciadi/mylinter/internal/noFmtImport" // Yeni bir analizör paketi varsayalım
"golang.org/x/tools/go/analysis/multichecker"
)
func main() {
multichecker.Main(
mylinter.Analyzer, // Magic number kontrolü
noFmtImport.Analyzer, // Fmt import kontrolü (yeni bir analizör)
// Diğer analizörlerinizi buraya ekleyin
)
}
Bu yaklaşım, kodunuzu modüler tutar ve her kuralın bağımsız olarak geliştirilmesini ve test edilmesini sağlar.
3. Tip Bilgilerini Kullanma (types.Info)
Bazen sadece AST yapısını incelemek yeterli olmayabilir; bir değişkenin veya ifadenin gerçek tipini bilmek isteyebilirsiniz. analysis.Pass nesnesi içindeki TypesInfo *types.Info alanı, derleyici tarafından toplanan tip bilgilerine erişim sağlar. Bu, daha karmaşık ve anlamsal analizler yapmanızı sağlar. Örneğin, bir fonksiyonun belirli bir arayüzü uygulayıp uygulamadığını veya bir değişkenin belirli bir türe sahip olup olmadığını kontrol edebilirsiniz.
// Örnek: Bir fonksiyonun dönüş tipini kontrol etme
func runWithTypes(pass *analysis.Pass) (interface{}, error) {
inspector := pass.ResultOf[inspect.Analyzer].(*ast.Inspector)
nodeFilter := []ast.Node{(*ast.FuncDecl)(nil)}
inspector.Preorder(nodeFilter, func(n ast.Node) {
funcDecl := n.(*ast.FuncDecl)
if funcDecl.Type.Results != nil && len(funcDecl.Type.Results.List) > 0 {
// İlk dönüş değerinin tipini alalım
firstResultField := funcDecl.Type.Results.List[0]
tv := pass.TypesInfo.TypeOf(firstResultField.Type)
if tv != nil && tv.String() == "error" {
// Eğer ilk dönüş tipi error ise, bir kontrol yapabiliriz.
// Örneğin, fonksiyonun adının "handle" ile başlayıp başlamadığını kontrol edebiliriz.
if !strings.HasPrefix(funcDecl.Name.Name, "handle") {
pass.Reportf(funcDecl.Pos(), "error döndüren fonksiyonların 'handle' ile başlaması önerilir")
}
}
}
})
return nil, nil
}
Tip bilgileri, Go’nun güçlü tip sisteminden tam olarak yararlanarak çok daha anlamlı linter kuralları yazmanıza olanak tanır.
4. Performans Optimizasyonu
Büyük kod tabanlarında linter’lar yavaş çalışabilir. Performansı artırmak için şunları göz önünde bulundurun:
- Gereksiz AST gezintilerinden kaçının: Sadece ihtiyacınız olan düğüm türlerini filtreleyin (
nodeFilter). - Erken çıkış yapın: Bir kural ihlali bulduğunuzda veya bir koşul sağlanmadığında fonksiyondan erken dönün.
- Önbellekleme: Eğer aynı bilgiyi tekrar tekrar hesaplamanız gerekiyorsa, bunu önbelleğe alın.
Bu ileri düzey teknikler, linter’ınızı sadece bir hata yakalama aracı olmaktan çıkarıp, projenizin özel ihtiyaçlarına göre şekillendirilmiş, akıllı bir kod asistanına dönüştürmenize yardımcı olacaktır.
Sonuç ve Sıkça Sorulan Sorular
Bu makalede, Go dilinde kendi linter’ınızı oluşturmanın temellerini adım adım inceledik. Neden linter’lara ihtiyaç duyduğumuzdan, Go’nun Soyut Sözdizimi Ağacı (AST) yapısına ve golang.org/x/tools/go/analysis framework’ünün gücüne kadar birçok konuyu ele aldık. “Magic Number” kontrolü gibi pratik bir kural geliştirerek öğrendiklerimizi pekiştirdik ve linter’ımızı nasıl test edip CI/CD süreçlerine entegre edeceğimizi gördük. Son olarak, konfigürasyon, çoklu kural yönetimi ve tip bilgisi kullanımı gibi ileri düzey ipuçlarıyla linter geliştirme becerilerimizi bir adım öteye taşıdık. Kendi linter’ınızı geliştirmek, sadece kod kalitenizi artırmakla kalmaz, aynı zamanda Go’nun iç işleyişi hakkında derinlemesine bilgi edinmenizi de sağlar. Bu yolculuk, daha temiz, daha tutarlı ve daha güvenilir Go kodları yazmanız için size güçlü bir araç seti sunacaktır. Unutmayın, iyi kod sadece çalışan kod değil, aynı zamanda okunabilir, sürdürülebilir ve hatasız olan koddur.
Sıkça Sorulan Sorular (SSS)
1. Neden kendi linter’ımı yazmalıyım, zaten birçok Go linter’ı yok mu?
Evet, Go ekosisteminde birçok harika linter (örneğin golangci-lint) bulunmaktadır. Ancak, bazen projenize veya ekibinize özel, çok niş kodlama standartları veya kısıtlamalarınız olabilir. Mevcut linter’ların kapsamadığı bu özel durumlar için kendi linter’ınızı yazmak, tam kontrol ve esneklik sağlar. Ayrıca, bu süreç Go’nun statik analiz yetenekleri hakkında derinlemesine bilgi edinmenize de yardımcı olur.
2. go/ast ve go/analysis arasındaki fark nedir?
go/ast paketi, Go kaynak kodunun Soyut Sözdizimi Ağacı (AST) temsilini sağlar ve bu ağaç üzerinde gezinmek için temel araçları içerir. go/analysis ise, bu AST üzerinde statik analizler yapmak için daha yüksek seviyeli bir framework (yazılım çerçevesi) sunar. go/analysis, analizörlerinizi yapılandırmanıza, diğer analizörlerle etkileşim kurmanıza ve standart bir şekilde raporlama yapmanıza olanak tanır. Kısacası, go/ast ham veriyi sağlarken, go/analysis bu veriyi işlemek için bir yapı ve araç seti sunar.
3. Linter’ımın performansını nasıl artırabilirim?
Performans optimizasyonu için birkaç strateji vardır: Gereksiz AST düğümlerini gezmekten kaçınmak için nodeFilter kullanın. Kural ihlali tespit edildiğinde veya bir koşul sağlanmadığında run fonksiyonundan erken dönün. Eğer aynı hesaplamaları tekrar tekrar yapıyorsanız, sonuçları önbelleğe alın. Büyük kod tabanlarında, sadece değişen dosyaları analiz etmek için akıllı önbellekleme mekanizmaları düşünebilirsiniz.
4. Linter’ımı diğer Go araçlarıyla nasıl entegre edebilirim?
Linter’ınızı multichecker.Main kullanarak diğer özel analizörlerinizle birleştirebilirsiniz. Ayrıca, golangci-lint gibi popüler linter araçları, özel analysis.Analyzer‘larınızı bir eklenti olarak çalıştırma yeteneği sunar. Bu, linter’ınızın daha geniş bir Go geliştirme ekosistemi içinde kullanılmasını sağlar. CI/CD sistemlerine entegrasyon için ise, makalede gösterildiği gibi basit bir go run ./cmd/mylinter ./... komutu yeterlidir.
5. Linter’ımda karmaşık tip kontrolleri yapmak mümkün mü?
Kesinlikle. analysis.Pass nesnesi içindeki TypesInfo *types.Info alanı, derleyici tarafından toplanan tüm tip bilgilerine erişmenizi sağlar. Bu sayede, değişkenlerin, fonksiyonların ve ifadelerin gerçek tiplerini, arayüz uygulamalarını ve diğer anlamsal bilgileri kontrol edebilirsiniz. Bu, sadece sözdizimsel değil, aynı zamanda anlamsal olarak doğru kod yazılmasını sağlayan çok daha güçlü kurallar oluşturmanıza olanak tanır.
#GoProgramlama #Linter #StatikAnaliz #KodKalitesi #YazılımGeliştirme #GoLang
