ruleguard: اختبارات ديناميكية لـ Go


في هذه المقالة ، سأتحدث عن مكتبة التحليل الثابت الجديدة ( go-ruleguard (والأداة المساعدة) التي تتكيف مع gogrep لاستخدامها داخل gogrep .


ميزة مميزة: أنت تصف قواعد التحليل الثابت في DSL خاص بـ Go-like ، والذي يتحول في بداية ruleguard إلى مجموعة من التشخيصات. ربما تكون هذه واحدة من أكثر الأدوات التي يمكن تكوينها بسهولة لتنفيذ عمليات التفتيش المخصصة لـ Go.


على سبيل المكافأة ، سنتحدث عن go/analysis والأسلاف السابقة .


تحليل ثابت القابلية للتوسعة


هناك العديد من البطانات لـ Go ، يمكن توسيع بعضها. عادة ، لتمديد linter ، تحتاج إلى كتابة رمز Go باستخدام API linter الخاص.


هناك طريقتان رئيسيتان: Go plugins و monolith. يتضمن المتراصة أن جميع الشيكات (بما في ذلك الشيكات الشخصية) متوفرة في مرحلة التجميع.


يتطلب revive الشيكات الجديدة ليتم تضمينها في نواة للتوسع. go-critic بالإضافة إلى ذلك المكونات الإضافية ، والتي تسمح لك بجمع الإضافات بغض النظر عن الرمز الرئيسي. يعني كل من هذه الطرق أنك تقوم بتنفيذ عمليات go/ast و go/types man على Go باستخدام API linter. حتى الشيكات البسيطة تتطلب الكثير من التعليمات البرمجية .


يهدف go/analysis إلى تبسيط الصورة من خلال حقيقة أن "إطار" اللنتير يصبح متطابقًا تقريبًا ، لكنه لا يحل مشكلة تعقيد التنفيذ التقني للتشخيصات نفسها.


استطراد على 'loader` و `go / package`


عندما تكتب محللًا لـ Go ، فإن هدفك النهائي هو التفاعل مع AST وأنواعه ، لكن قبل أن تتمكن من القيام بذلك ، يجب أن يتم "تحميل" الكود المصدري بالطريقة الصحيحة. لتبسيط ، ويشمل مفهوم التحميل تحليل ، والتحقق من النوع ، واستيراد التبعيات .


الخطوة الأولى في تبسيط خط الأنابيب هذا كانت حزمة go/loader ، والتي ستتيح لك "تنزيل" كل ما تحتاجه من خلال مكالمات عدة. كان كل شيء جيدًا تقريبًا ، ثم أصبح مهملاً لصالح go/packages . go/packages لها واجهة برمجة تطبيقات محسّنة قليلاً ، ومن الناحية النظرية ، تعمل بشكل جيد مع الوحدات.


الآن ، من الأفضل عدم استخدام أي مما سبق مباشرة لكتابة المحللون ، لأن go/analysis أعطت go/packages شيئًا لم يكن لدى أي من الحلول السابقة - هيكل لبرنامجك. الآن يمكننا استخدام نموذج go/analysis إملاءه وإعادة تحليل المحللين بشكل أكثر كفاءة. يحتوي هذا النموذج على نقاط مثيرة للجدل ، على سبيل المثال ، go/analysis مناسب تمامًا للتحليل على مستوى حزمة واحدة وتوابعها ، ولكن لإجراء تحليل عالمي عليها دون حيل هندسية الماكرة لن يكون بالأمر السهل.


go/analysis يبسط أيضا اختبار محلل .




ما هو حارس القانون؟



go-ruleguard هي أداة مساعدة للتحليل الثابت لا تتضمن فحصًا واحدًا افتراضيًا.


ruleguard تحميل قواعد ruleguard في البداية ، من ملف gorules خاص gorules أنماط الأكواد التي يجب أن تصدر التحذيرات إليها. يمكن تحرير هذا الملف بحرية بواسطة مستخدمي ruleguard .


ليس من الضروري gorules برنامج التحكم للاتصال بالشيكات الجديدة ، لذلك يمكن gorules القواعد من gorules ديناميكية .


يشبه برنامج التحكم ruleguard هذا:


 package main import ( "github.com/quasilyte/go-ruleguard/analyzer" "golang.org/x/tools/go/analysis/singlechecker" ) func main() { singlechecker.Main(analyzer.Analyzer) } 

في الوقت نفسه ، analyzer تطبيق analyzer خلال حزمة ruleguard ، والتي يجب عليك استخدامها إذا كنت ترغب في استخدامها كمكتبة.


سيادة الحرس VS إحياء


خذ مثالًا بسيطًا لكنه حقيقي: افترض أننا نريد تجنب runtime.GC() المكالمات runtime.GC() في برامجنا. في إحياء هناك بالفعل تشخيص منفصل لهذا ، ويسمى "call-to-gc" .


تطبيق Call-to-gc (70 خطًا في Elven)


 package rule import ( "go/ast" "github.com/mgechev/revive/lint" ) // CallToGCRule lints calls to the garbage collector. type CallToGCRule struct{} // Apply applies the rule to given file. func (r *CallToGCRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure { var failures []lint.Failure onFailure := func(failure lint.Failure) { failures = append(failures, failure) } var gcTriggeringFunctions = map[string]map[string]bool{ "runtime": map[string]bool{"GC": true}, } w := lintCallToGC{onFailure, gcTriggeringFunctions} ast.Walk(w, file.AST) return failures } // Name returns the rule name. func (r *CallToGCRule) Name() string { return "call-to-gc" } type lintCallToGC struct { onFailure func(lint.Failure) gcTriggeringFunctions map[string]map[string]bool } func (w lintCallToGC) Visit(node ast.Node) ast.Visitor { ce, ok := node.(*ast.CallExpr) if !ok { return w // nothing to do, the node is not a call } fc, ok := ce.Fun.(*ast.SelectorExpr) if !ok { return nil // nothing to do, the call is not of the form pkg.func(...) } id, ok := fc.X.(*ast.Ident) if !ok { return nil // in case X is not an id (it should be!) } fn := fc.Sel.Name pkg := id.Name if !w.gcTriggeringFunctions[pkg][fn] { return nil // it isn't a call to a GC triggering function } w.onFailure(lint.Failure{ Confidence: 1, Node: node, Category: "bad practice", Failure: "explicit call to the garbage collector", }) return w } 



قارن الآن كيف يتم ذلك في go-ruleguard :


 package gorules import "github.com/quasilyte/go-ruleguard/dsl/fluent" func callToGC(m fluent.Matcher) { m.Match(`runtime.GC()`).Report(`explicit call to the garbage collector`) } 

لا شيء أكثر من ذلك ، ما هو مهم حقًا - runtime.GC ورسالة يجب إصدارها في حالة وجود قاعدة.


قد تسأل: هل هذا كل شيء؟ لقد بدأت على وجه التحديد بمثال بسيط لإظهار مقدار الشفرة التي قد تكون مطلوبة لتشخيص تافه للغاية في حالة النهج التقليدي. أعدك أنه سيكون هناك المزيد من الأمثلة المثيرة.


بداية سريعة


لدى rangeExprCopy go-critic تشخيص rangeExprCopy يعثر على نسخ صفيف غير متوقعة في الكود.


يتم تكرار هذا الرمز على نسخة من المصفوفة:


 var xs [2048]byte for _, x := range xs { // Copies 2048 bytes // Loop body. } 

إصلاح هذه المشكلة هو إضافة حرف واحد:


  var xs [2048]byte - for _, x := range xs { // Copies 2048 bytes + for _, x := range &xs { // No copy // Loop body. } 

على الأرجح ، لا تحتاج إلى هذا النسخ ، كما أن أداء الإصدار الذي تم تصحيحه أفضل دائمًا. يمكنك الانتظار حتى يتحسن برنامج التحويل البرمجي Go ، أو يمكنك اكتشاف مثل هذه الأماكن في الكود وتصحيحها اليوم باستخدام نفس go-critic .


يمكن تنفيذ هذا التشخيص في اللغة rules.go (ملف rules.go ):


 package gorules import "github.com/quasilyte/go-ruleguard/dsl/fluent" func _(m fluent.Matcher) { m.Match(`for $_, $_ := range $x { $*_ }`, `for $_, $_ = range $x { $*_ }`). Where(m["x"].Addressable && m["x"].Type.Size >= 128). Report(`$x copy can be avoided with &$x`). At(m["x"]). Suggest(`&$x`) } 

تبحث القاعدة for-range جميع حلقات for-range حيث يتم استخدام كلا المتغيرين القابلين للتكرار (هذه هي الحالة التي تؤدي إلى النسخ). يجب أن يكون التعبير القابل للتكرار $x addressable ويجب أن يكون أكبر من الحد المحدد بالبايت.


يعرّف Report() الرسالة التي سيتم إصدارها إلى المستخدم ، ويصف Suggest() قالب quickfix الذي يمكن استخدامه في برنامج التحرير الخاص بك عبر gopls (LSP) ، وكذلك بشكل تفاعلي إذا ruleguard استدعاء ruleguard باستخدام الوسيطة ruleguard (سنعود إلى هذا). At() يعلق التحذير و quickfix إلى جزء معين من القالب. نحتاج هذا لاستبدال $x بـ &$x ، بدلاً من إعادة كتابة الحلقة بأكملها.


يقبل كل من Report() Suggest() سلسلة يمكن فيها تحريف التعبيرات التي تم التقاطها بواسطة القالب من Match() . المتغير المحدد مسبقًا يعني "كل جزء تم التقاطه" (مثل $0 في التعبيرات العادية).


قم rangecopy.go ملف rangecopy.go :


 package example // sizeof(builtins[...]) = 240 on x86-64 var builtins = [...]string{ "append", "cap", "close", "complex", "copy", "delete", "imag", "len", "make", "new", "panic", "print", "println", "real", "recover", } func builtinID(name string) int { for i, s := range builtins { if s == name { return i } } return -1 } 

الآن يمكننا تشغيل ruleguard :


 $ ruleguard -rules rules.go -fix rangecopy.go rangecopy.go:12:20: builtins copy can be avoided with &builtins 

إذا نظرنا بعد ذلك إلى rangecopy.go ، rangecopy.go إصدارًا ثابتًا ، لأنه تم استدعاء ruleguard باستخدام المعلمة ruleguard .


أبسط القواعد يمكن تصحيحها دون إنشاء ملف gorules :


 $ ruleguard -c 1 -e 'm.Match(`return -1`)' rangecopy.go rangecopy.go:17:2: return -1 16 } 17 return -1 18 } 

بفضل استخدام go/analysis/singlechecker ، لدينا الخيار -c ، والذي يسمح لنا بعرض خطوط السياق المحددة مع التحذير نفسه. تعتبر السيطرة على هذه المعلمة غير بديهية بعض الشيء: القيمة الافتراضية هي -c=-1 ، مما يعني "بلا سياق" ، و -c=0 سينتج سطرًا واحدًا من السياق (الخط المشار إليه بواسطة التشخيص).


وهنا بعض gorules أكثر إثارة للاهتمام:


  • اكتب القوالب التي تتيح لك تحديد الأنواع المتوقعة. على سبيل المثال ، توضح map[$t]$t التعبيرية map[$t]$t جميع المخططات التي يتطابق نوع القيمة مع نوع المفتاح ، بينما يلتقط *[$len]$elem جميع المؤشرات إلى صفائف.
  • ضمن وظيفة واحدة ، قد يكون هناك عدة قواعد ،
    والوظائف نفسها يجب أن تسمى مجموعات القاعدة .
  • يتم تطبيق القواعد في المجموعة واحدة تلو الأخرى ، بالترتيب الذي تم تعريفها به. القاعدة الأولى التي يتم تشغيلها تلغي المقارنة مع القواعد المتبقية. هذا مهم ليس كثيرًا للتحسين بقدر أهمية تخصيص قواعد لحالات معينة. مثال على ذلك ، حيث يكون هذا مفيدًا هو قاعدة إعادة كتابة $x=$x+$y إلى $x+=$y ، بالنسبة للحالة مع $y=1 تريد تقديم $x++ ، وليس $x+=1 .

يمكن العثور على مزيد من المعلومات حول DSL المستخدمة في docs/gorules.md .


المزيد من الأمثلة


 package gorules import "github.com/quasilyte/go-ruleguard/dsl/fluent" func exampleGroup(m fluent.Matcher) { //     json.Decoder. // . http://golang.org/issue/36225 m.Match(`json.NewDecoder($_).Decode($_)`). Report(`this json.Decoder usage is erroneous`) //   unconvert,    . m.Match(`time.Duration($x) * time.Second`). Where(m["x"].Const). Suggest(`$x * time.Second`) //   fmt.Sprint()    String(), //   $x  . m.Match(`fmt.Sprint($x)`). Where(m["x"].Type.Implements(`fmt.Stringer`)). Suggest(`$x.String()`) //   . m.Match(`!($x != $y)`).Suggest(`$x == $y`) m.Match(`!($x == $y)`).Suggest(`$x != $y`) } 

إذا لم يكن هناك استدعاء Report() للقاعدة ، فسيتم استخدام إخراج الرسالة من Suggest() . هذا يسمح في بعض الحالات بتجنب الازدواجية.


يمكن للمرشحات اكتب والنماذج الفرعية التحقق من الخصائص المختلفة. على سبيل المثال ، تكون خصائص Pure و Const مفيدة:


  • Var.Pure يعني أن التعبير ليس له آثار جانبية.
  • يعني Var.Const أنه يمكن استخدام التعبير في سياق ثابت (على سبيل المثال ، بعد صفيف).

للأسماء package-qualified للحزم في شروط Where() ، تحتاج إلى استخدام الأسلوب Import() . للراحة ، تم استيراد جميع الحزم القياسية لك ، لذلك في المثال أعلاه لا نحتاج إلى القيام بواردات إضافية.


go/analysis الإجراءات quickfix


quickfix دعم quickfix عن طريق go/analysis بالنسبة لنا.


في نموذج go/analysis ، يولد المحلل التشخيصات والحقائق . يتم إرسال التشخيص إلى المستخدمين ، والوقائع مخصصة للاستخدام من قبل المحللين الآخرين.


يمكن أن تحتوي التشخيصات على مجموعة من الإصلاحات المقترحة ، يصف كل منها كيفية تغيير أكواد المصدر في النطاق المحدد من أجل إصلاح المشكلة التي عثر عليها التشخيص.


الوصف الرسمي متاح في go/analysis/doc/suggested_fixes.md .


استنتاج



جرِّب ruleguard في مشاريعك ، وإذا وجدت خطأً أو كنت ترغب في طلب ميزة جديدة ، فافتح المشكلة .


إذا كنت لا تزال تجد صعوبة في التوصل إلى تطبيق ruleguard ، فإليك بعض الأمثلة:


  • قم بتطبيق تشخيصاتك الخاصة لـ Go.
  • ترقية تلقائيا أو refactor رمز مع -fix .
  • جمع إحصائيات الكود باستخدام معالجة -json .

خطط التطوير ruleguard في المستقبل القريب:



روابط وموارد مفيدة


Source: https://habr.com/ru/post/ar481696/


All Articles