Linters ดำเนินการโดย Ruby Libraries

เผยแพร่แล้ว: 2022-03-11

เมื่อคุณได้ยินคำว่า " linter " หรือ " lint " คุณคงมีความคาดหวังอยู่แล้วว่าเครื่องมือดังกล่าวทำงานอย่างไรหรือควรทำอย่างไร

คุณอาจกำลังนึกถึง Rubocop ซึ่งผู้พัฒนา Toptal คนใดคนหนึ่งดูแล หรือของ JSLint, ESLint หรือสิ่งที่ไม่ค่อยเป็นที่รู้จักหรือไม่ค่อยเป็นที่นิยม

บทความนี้จะแนะนำคุณเกี่ยวกับ Linters ประเภทต่างๆ พวกเขาไม่ตรวจสอบไวยากรณ์ของโค้ดและไม่ตรวจสอบ Abstract-Syntax-Tree แต่จะตรวจสอบโค้ด พวกเขาตรวจสอบว่าการใช้งานเป็นไปตามอินเทอร์เฟซบางอย่างหรือไม่ ไม่ใช่แค่คำศัพท์ (ในแง่ของการพิมพ์แบบเป็ดและอินเทอร์เฟซแบบคลาสสิก) แต่บางครั้งก็มีความหมายด้วย

เพื่อทำความคุ้นเคยกับพวกเขา มาวิเคราะห์ตัวอย่างที่ใช้งานได้จริง หากคุณไม่ใช่มืออาชีพ Rails ตัวยง คุณอาจต้องการอ่านสิ่งนี้ก่อน

มาเริ่มกันที่ Lint พื้นฐานกัน

ActiveModel::Lint::Tests

พฤติกรรมของผ้าสำลีนี้อธิบายโดยละเอียดในเอกสารทางการของ Rails:

“คุณสามารถทดสอบว่าวัตถุนั้นสอดคล้องกับ Active Model API หรือไม่ โดยรวม ActiveModel::Lint::Tests ใน TestCase ของคุณ ซึ่งจะรวมถึงการทดสอบที่บอกคุณว่าอ็อบเจ็กต์ของคุณเป็นไปตามข้อกำหนดอย่างสมบูรณ์หรือไม่ หรือหากไม่ใช่ แสดงว่า API ใดไม่ได้ใช้งาน โปรดทราบว่าไม่จำเป็นต้องใช้อ็อบเจ็กต์เพื่อใช้งาน API ทั้งหมดเพื่อทำงานกับ Action Pack โมดูลนี้มีจุดประสงค์เพื่อให้คำแนะนำในกรณีที่คุณต้องการให้ฟีเจอร์ทั้งหมดพร้อมใช้งานทันที”

ดังนั้น หากคุณกำลังใช้งานคลาสและต้องการใช้กับฟังก์ชัน Rails ที่มีอยู่ เช่น redirect_to, form_for คุณต้องใช้วิธีสองวิธี ฟังก์ชันนี้ไม่จำกัดเฉพาะวัตถุ ActiveRecord มันสามารถทำงานกับวัตถุของคุณได้เช่นกัน แต่พวกมันจำเป็นต้องเรียนรู้ที่จะต้มตุ๋นอย่างถูกต้อง

การดำเนินการ

การใช้งานค่อนข้างตรงไปตรงมา เป็นโมดูลที่สร้างขึ้นเพื่อรวมในกรณีทดสอบ วิธีการที่เริ่มต้นด้วย test_ จะถูกนำไปใช้โดยกรอบงานของคุณ คาดว่าตัวแปรอินสแตนซ์ @model จะถูกตั้งค่าโดยผู้ใช้ก่อนการทดสอบ:

 module ActiveModel module Lint module Tests def test_to_key assert_respond_to model, :to_key def model.persisted?() false end assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false" end def test_to_param assert_respond_to model, :to_param def model.to_key() [1] end def model.persisted?() false end assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false" end ... private def model assert_respond_to @model, :to_model @model.to_model end

การใช้งาน

 class Person def persisted? false end def to_key nil end def to_param nil end # ... end # test/models/person_test.rb require "test_helper" class PersonTest < ActiveSupport::TestCase include ActiveModel::Lint::Tests setup do @model = Person.new end end

ActiveModel::Serializer::Lint::Tests

เครื่องซีเรียลไลเซอร์รุ่นที่ใช้งานไม่ใช่เรื่องใหม่ แต่เราสามารถเรียนรู้จากพวกเขาต่อไปได้ คุณรวม ActiveModel::Serializer::Lint::Tests เพื่อตรวจสอบว่าวัตถุสอดคล้องกับ Active Model Serializers API หรือไม่ หากไม่เป็นเช่นนั้น การทดสอบจะระบุว่าส่วนใดขาดหายไป

อย่างไรก็ตาม ในเอกสาร คุณจะพบคำเตือนที่สำคัญว่าไม่ตรวจสอบความหมาย:

“การทดสอบเหล่านี้ไม่ได้พยายามกำหนดความถูกต้องตามความหมายของค่าที่ส่งคืน ตัวอย่างเช่น คุณสามารถใช้ serializable_hash เพื่อส่งคืน {} เสมอ และการทดสอบจะผ่านไป มันขึ้นอยู่กับคุณที่จะทำให้แน่ใจว่าค่านิยมมีความหมายเชิงความหมาย”

กล่าวคือ เรากำลังตรวจสอบรูปร่างของอินเทอร์เฟซเท่านั้น ตอนนี้เรามาดูกันว่ามันใช้งานอย่างไร

การดำเนินการ

สิ่งนี้คล้ายกันมากกับสิ่งที่เราเห็นเมื่อสักครู่นี้ด้วยการใช้งาน ActiveModel::Lint::Tests แต่เข้มงวดกว่าเล็กน้อยในบางกรณีเนื่องจากจะตรวจสอบ arity หรือคลาสของค่าที่ส่งคืน:

 module ActiveModel class Serializer module Lint module Tests # Passes if the object responds to <tt>read_attribute_for_serialization</tt> # and if it requires one argument (the attribute to be read). # Fails otherwise. # # <tt>read_attribute_for_serialization</tt> gets the attribute value for serialization # Typically, it is implemented by including ActiveModel::Serialization. def test_read_attribute_for_serialization assert_respond_to resource, :read_attribute_for_serialization, 'The resource should respond to read_attribute_for_serialization' actual_arity = resource.method(:read_attribute_for_serialization).arity # using absolute value since arity is: # 1 for def read_attribute_for_serialization(name); end # -1 for alias :read_attribute_for_serialization :send assert_equal 1, actual_arity.abs, "expected #{actual_arity.inspect}.abs to be 1 or -1" end # Passes if the object's class responds to <tt>model_name</tt> and if it # is in an instance of +ActiveModel::Name+. # Fails otherwise. # # <tt>model_name</tt> returns an ActiveModel::Name instance. # It is used by the serializer to identify the object's type. # It is not required unless caching is enabled. def test_model_name resource_class = resource.class assert_respond_to resource_class, :model_name assert_instance_of resource_class.model_name, ActiveModel::Name end ...

การใช้งาน

ต่อไปนี้คือตัวอย่างวิธีที่ ActiveModelSerializers ใช้ผ้าสำลีโดยรวมไว้ในกรณีทดสอบ:

 module ActiveModelSerializers class ModelTest < ActiveSupport::TestCase include ActiveModel::Serializer::Lint::Tests setup do @resource = ActiveModelSerializers::Model.new end def test_initialization_with_string_keys klass = Class.new(ActiveModelSerializers::Model) do attributes :key end value = 'value' model_instance = klass.new('key' => value) assert_equal model_instance.read_attribute_for_serialization(:key), value end

ชั้นวาง::ผ้าสำลี

ตัวอย่างก่อนหน้านี้ไม่สนใจ ความหมาย

อย่างไรก็ตาม Rack::Lint เป็นสัตว์ร้ายที่ต่างไปจากเดิมอย่างสิ้นเชิง มิดเดิลแวร์ของแร็คเป็นแร็คที่คุณสามารถห่อหุ้มแอปพลิเคชันของคุณได้ มิดเดิลแวร์จะทำหน้าที่เป็นตัวคั่นกลางในกรณีนี้ linter จะตรวจสอบว่าคำขอและการตอบสนองถูกสร้างขึ้นตามข้อกำหนดของ Rack หรือไม่ สิ่งนี้มีประโยชน์หากคุณกำลังใช้งานเซิร์ฟเวอร์ Rack (เช่น Puma) ที่จะให้บริการแอปพลิเคชัน Rack และคุณต้องการให้แน่ใจว่าคุณปฏิบัติตามข้อกำหนดของ Rack

อีกวิธีหนึ่งคือใช้เมื่อคุณใช้งานแอปพลิเคชันเปล่าๆ และคุณต้องการแน่ใจว่าคุณจะไม่ทำผิดพลาดง่ายๆ ที่เกี่ยวข้องกับโปรโตคอล HTTP

การดำเนินการ

 module Rack class Lint def initialize(app) @app = app @content_length = nil end def call(env = nil) dup._call(env) end def _call(env) raise LintError, "No env given" unless env check_env env env[RACK_INPUT] = InputWrapper.new(env[RACK_INPUT]) env[RACK_ERRORS] = ErrorWrapper.new(env[RACK_ERRORS]) ary = @app.call(env) raise LintError, "response is not an Array, but #{ary.class}" unless ary.kind_of? Array raise LintError, "response array has #{ary.size} elements instead of 3" unless ary.size == 3 status, headers, @body = ary check_status status check_headers headers hijack_proc = check_hijack_response headers, env if hijack_proc && headers.is_a?(Hash) headers[RACK_HIJACK] = hijack_proc end check_content_type status, headers check_content_length status, headers @head_request = env[REQUEST_METHOD] == HEAD [status, headers, self] end ## === The Content-Type def check_content_type(status, headers) headers.each { |key, value| ## There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx, 204 or 304. if key.downcase == "content-type" if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i raise LintError, "Content-Type header found in #{status} response, not allowed" end return end } end ## === The Content-Length def check_content_length(status, headers) headers.each { |key, value| if key.downcase == 'content-length' ## There must not be a <tt>Content-Length</tt> header when the +Status+ is 1xx, 204 or 304. if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i raise LintError, "Content-Length header found in #{status} response, not allowed" end @content_length = value end } end ...

การใช้งานในแอปของคุณ

สมมติว่าเราสร้างจุดสิ้นสุดที่ง่ายมาก บางครั้งควรตอบสนองด้วย "ไม่มีเนื้อหา" แต่เราได้ทำผิดพลาดโดยเจตนาและเราจะส่งเนื้อหาบางส่วนใน 50% ของกรณี:

 # foo.rb # run with rackup foo.rb Foo = Rack::Builder.new do use Rack::Lint use Rack::ContentLength app = proc do |env| if rand > 0.5 no_content = Rack::Utils::HTTP_STATUS_CODES.invert['No Content'] [no_content, { 'Content-Type' => 'text/plain' }, ['bummer no content with content']] else ok = Rack::Utils::HTTP_STATUS_CODES.invert['OK'] [ok, { 'Content-Type' => 'text/plain' }, ['good']] end end run app end.to_app

ในกรณีดังกล่าว Rack::Lint จะสกัดกั้นการตอบสนอง ตรวจสอบ และยกข้อยกเว้น:

 Rack::Lint::LintError: Content-Type header found in 204 response, not allowed /Users/dev/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/rack-2.2.3/lib/rack/lint.rb:21:in `assert' /Users/dev/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/rack-2.2.3/lib/rack/lint.rb:710:in `block in check_content_type'

การใช้ใน Puma

ในตัวอย่างนี้ เราจะเห็นว่า Puma ล้อมโปรแกรม lambda { |env| [200, { "X-Header" => "Works" }, ["Hello"]] } lambda { |env| [200, { "X-Header" => "Works" }, ["Hello"]] } อันดับแรกใน ServerLint (ซึ่งสืบทอดมาจาก Rack::Lint ) จากนั้นใน ErrorChecker

ผ้าสำลีทำให้เกิดข้อยกเว้นในกรณีที่ไม่ปฏิบัติตามข้อกำหนด ตัวตรวจสอบจับข้อยกเว้นและส่งคืนรหัสข้อผิดพลาด 500 รหัสทดสอบตรวจสอบว่าข้อยกเว้นไม่เกิดขึ้น:

 class TestRackServer < Minitest::Test class ErrorChecker def initialize(app) @app = app @exception = nil end attr_reader :exception, :env def call(env) begin @app.call(env) rescue Exception => e @exception = e [ 500, {}, ["Error detected"] ] end end end class ServerLint < Rack::Lint def call(env) check_env env @app.call(env) end end def setup @simple = lambda { |env| [200, { "X-Header" => "Works" }, ["Hello"]] } @server = Puma::Server.new @simple port = (@server.add_tcp_listener "127.0.0.1", 0).addr[1] @tcp = "http://127.0.0.1:#{port}" @stopped = false end def test_lint @checker = ErrorChecker.new ServerLint.new(@simple) @server.app = @checker @server.run hit(["#{@tcp}/test"]) stop refute @checker.exception, "Checker raised exception" end

นั่นคือวิธีที่ Puma ได้รับการยืนยันว่าเข้ากันได้กับ Rack ที่ผ่านการรับรอง

RailsEventStore - ที่เก็บ Lint

Rails Event Store เป็นไลบรารีสำหรับเผยแพร่ ใช้งาน จัดเก็บ และเรียกข้อมูลกิจกรรม มีจุดมุ่งหมายเพื่อช่วยคุณในการใช้สถาปัตยกรรมที่ขับเคลื่อนด้วยเหตุการณ์สำหรับแอปพลิเคชัน Rails ของคุณ เป็นไลบรารีแบบโมดูลาร์ที่สร้างขึ้นด้วยส่วนประกอบขนาดเล็ก เช่น ที่เก็บข้อมูล ผู้ทำแผนที่ ผู้มอบหมายงาน ตัวจัดกำหนดการ การสมัครรับข้อมูล และเครื่องซีเรียลไลเซอร์ แต่ละองค์ประกอบสามารถมีการใช้งานแทนกันได้

ตัวอย่างเช่น ที่เก็บเริ่มต้นใช้ ActiveRecord และถือว่าเค้าโครงตารางบางอย่างสำหรับการจัดเก็บเหตุการณ์ อย่างไรก็ตาม การนำไปใช้ของคุณสามารถใช้ ROM หรือทำงานในหน่วยความจำได้โดยไม่ต้องจัดเก็บเหตุการณ์ ซึ่งมีประโยชน์สำหรับการทดสอบ

แต่คุณจะทราบได้อย่างไรว่าส่วนประกอบที่คุณใช้ทำงานในลักษณะที่ห้องสมุดคาดหวังหรือไม่ โดยใช้ผ้าสำลีที่ให้มาแน่นอน และมันยิ่งใหญ่มาก ครอบคลุมประมาณ 80 กรณี บางส่วนค่อนข้างง่าย:

 specify 'adds an initial event to a new stream' do repository.append_to_stream([event = SRecord.new], stream, version_none) expect(read_events_forward(repository).first).to eq(event) expect(read_events_forward(repository, stream).first).to eq(event) expect(read_events_forward(repository, stream_other)).to be_empty end

และบางส่วนก็ซับซ้อนกว่าเล็กน้อยและเกี่ยวข้องกับเส้นทางที่ไม่มีความสุข:

 it 'does not allow linking same event twice in a stream' do repository.append_to_stream( [SRecord.new(event_id: "a1b49edb")], stream, version_none ).link_to_stream(["a1b49edb"], stream_flow, version_none) expect do repository.link_to_stream(["a1b49edb"], stream_flow, version_0) end.to raise_error(EventDuplicatedInStream) end

ที่โค้ด Ruby เกือบ 1,400 บรรทัด ฉันเชื่อว่ามันเป็น linter ที่ใหญ่ที่สุดที่เขียนด้วย Ruby แต่ถ้ารู้ว่าใหญ่กว่านี้ บอกได้นะคะ ส่วนที่น่าสนใจคือมันเกี่ยวกับความหมาย 100%

มันทดสอบอินเทอร์เฟซอย่างหนักเช่นกัน แต่ฉันจะบอกว่านั่นเป็นความคิดภายหลังตามขอบเขตของบทความนี้

การดำเนินการ

linter ที่เก็บถูกใช้งานโดยใช้ฟังก์ชัน RSpec Shared Examples:

 module RubyEventStore ::RSpec.shared_examples :event_repository do let(:helper) { EventRepositoryHelper.new } let(:specification) { Specification.new(SpecificationReader.new(repository, Mappers::NullMapper.new)) } let(:global_stream) { Stream.new(GLOBAL_STREAM) } let(:stream) { Stream.new(SecureRandom.uuid) } let(:stream_flow) { Stream.new('flow') } # ... it 'just created is empty' do expect(read_events_forward(repository)).to be_empty end specify 'append_to_stream returns self' do repository .append_to_stream([event = SRecord.new], stream, version_none) .append_to_stream([event = SRecord.new], stream, version_0) end # ...

การใช้งาน

linter นี้ คล้ายกับวิธีอื่นๆ คาดหวังให้คุณจัดเตรียมวิธีการบางอย่าง ที่สำคัญที่สุดคือ repository ซึ่งส่งคืนการใช้งานที่จะได้รับการยืนยัน รวมตัวอย่างการทดสอบโดยใช้เมธอด RSpec include_examples ในตัว:

 RSpec.describe EventRepository do include_examples :event_repository let(:repository) { EventRepository.new(serializer: YAML) } end

ห่อ

อย่างที่คุณเห็น “ linter” มีความหมายที่กว้างกว่าที่เราคิดไว้เล็กน้อย เมื่อใดก็ตามที่คุณใช้ไลบรารี่ที่คาดว่าจะมีผู้ทำงานร่วมกันที่สามารถใช้แทนกันได้ เราขอแนะนำให้คุณพิจารณาจัดหา linter

แม้ว่าชั้นเรียนเดียวที่ผ่านการทดสอบดังกล่าวในตอนเริ่มต้นจะเป็นชั้นเรียนที่ห้องสมุดจัดเตรียมไว้ให้ด้วย นั่นเป็นสัญญาณว่าคุณในฐานะวิศวกรซอฟต์แวร์ให้ความสำคัญกับการขยายอย่างจริงจัง นอกจากนี้ยังท้าทายให้คุณนึกถึงอินเทอร์เฟซสำหรับแต่ละส่วนประกอบในโค้ดของคุณ ไม่ใช่โดยบังเอิญแต่เป็นอย่างมีสติ

ทรัพยากร

  • ActiveModel: ทำให้ Ruby Object รู้สึกเหมือน ActiveRecord
  • RailsCast เกี่ยวกับ Active Model
  • การใช้ผ้าสำลีแบบเต็มแร็ค
  • การใช้แร็คผ้าสำลีใน Puma
  • เรียนรู้เพิ่มเติมเกี่ยวกับ Rails Event Store