Поддержка стандартов командного кодирования: создание правил Lint.

В предыдущем посте мы узнали, как создать простую проверку, используя уже существующие средства тестирования языка. Мы также упомянули о существовании инструментов Lint: в VA Smalltalk этот инструмент называется SmallLint, а в Pharo он называется Quality Assistant. Оба инструмента - следуя философии открытой экосистемы - позволяют легко расширять предоставляемые правила.

Одним из преимуществ автоматизации стандартов командного кодирования является то, что, когда приходит новый член команды, каждый раз, когда единица работы закрывается, эти проверки запускаются, и это помогает новичку адаптироваться к особенностям команды. В этом смысле важно, чтобы каждая реализованная проверка или правило сопровождалась обоснованием, объясняющим, почему команда считает это хорошей или плохой практикой.

Есть несколько примеров этой идеи в дикой природе. Например, веб-фреймворк Seaside поставляется с собственной программой проверки под названием Slime, которая расширяет правила SmallLint.

Давайте теперь посмотрим на некоторые правила, которые добавила наша команда, и их обоснование.

В этих примерах будет использоваться синтаксис Rewrite Engine, поэтому минимальное понимание этого является плюсом. Если вы этого не знаете: вкратце `@variable соответствует практически любому выражению в дереве синтаксического анализа.

Первые правила, которые мы увидим, имеют такое же обоснование: Не рекомендуется возвращать (или назначать) nil в результате отправки сообщения. Потому что, как правило, это вызывает множество неподдерживаемых нулевых проверок отправителей (или красивых ошибок во время выполнения: P).

Использует #ifTrue: или #ifFalse: по отдельности

Обоснование здесь состоит в том, чтобы избегать использования ifTrue: или ifFalse: по отдельности и возврата, присвоения переменной или передачи в качестве параметра результата, потому что в случае, если блок не оценивается, результатом будет nil.

ParseTreeLintRule class>>usesIfTrueOrIfFalseIndividually
  ^self createParseTreeRule: #(
    ‘^`@object ifTrue: ``@block’ 
    ‘^`@object ifFalse: ``@block’
    ‘`var := ``@object ifTrue: ``@block’ 
    ‘`var := ``@object ifFalse: ``@block’
    ‘``@object `message: (``@anotherObject ifTrue: ``@block)’
    ‘``@object `message: (``@anotherObject ifFalse: ``@block)’
    ) name: ‘Uses #ifTrue: or #ifFalse: individually’

Каждая строка внутри массива представляет собой выражение сопоставления дерева синтаксического анализа.

Использует #ifNotNil: индивидуально

Обоснование здесь аналогично предыдущему случаю. Поскольку в случае, если получатель nil, блок не оценивается, и результат будет nil.

ParseTreeLintRule class>>usesIfTrueOrIfFalseIndividually
^self createParseTreeRule: #(
    ‘^`@object ifNotNil: ``@block’ 
    ‘`var := ``@object ifNotNil: ``@block’ 
    ‘``@object `message: (``@anotherObject ifNotNil: ``@block)’
    ) name: ‘Uses #ifNotNil: individually’

Возвращает или присваивает #detect: ifFound: result

Этот метод является расширением, которое мы сделали для иерархии Коллекции (Pharo теперь включает его в библиотеку базовых классов), чтобы помочь, когда вы хотите что-то сделать с результатом обнаружения: но на всякий случай, если найден соответствующий объект.

Обоснование здесь аналогично предыдущему случаю (обратите внимание, что в Pharo он вернет коллекцию, а не nil, как это делает наша исходная реализация). В любом случае, если вы отправляете это сообщение, вы не должны использовать результат.

ParseTreeLintRule class>>returnsOrAssignsDetectIfFound
^self createParseTreeRule: #(
  '^`@receiver detect: `@detectBlock ifFound: `@foundBlock'
  '`var := `@receiver detect: `@detectBlock ifFound: `@foundBlock'
  ) name: ‘Return or Assigns #detect:ifFound:’

Правила выявления намерений

Следующие кейсы не обнаруживают ошибок, но улучшают читаемость результирующего кода.

Использует #detect: ifNone: вместо #anySatisfy: / #noneSatisfy:

Это правило выявляет некоторые ошибки новичков, в основном из-за того, что они пропустили какое-то сообщение в Collection API, которое лучше подходит для ситуации.

ParseTreeLintRule class>>returnsOrAssignsDetectIfFound
^self createParseTreeRule: #(
 '(``@col detect: [:`e| |`@temps| ``@.exp] ifNone: [nil]) isNil'
 '(``@col detect: [:`e| |`@temps| ``@.exp] ifNone: [nil]) = nil'
 '(``@col detect: [:`e| |`@temps| ``@.exp] ifNone: [nil]) == nil'
 '(``@col detect: [:`e| |`@temps| ``@.exp] ifNone: [nil]) notNil'
 '(``@col detect: [:`e| |`@temps| ``@.exp] ifNone: [nil]) ~= nil'
 '(``@col detect: [:`e| |`@temps| ``@.exp] ifNone: [nil]) ~~ nil' ) 
name: ‘Uses #detect:ifNone: instead of #anySatisfy:/#noneSatisfy:’

Когда мы обнаруживаем что-то подобное при проверке кода, по возможности мы добавляем новое правило, предлагающее замену кода, чтобы это никогда не повторилось.

Назначение и тестирование

Мы обнаружили, что иногда трудно понять намерение кода, когда объект присваивается некоторой переменной, а затем сразу же отправляется сообщение о том же выражении. Поэтому для ясности мы предпочитаем сначала выполнить присвоение и отправить сообщение в следующем операторе.

ParseTreeLintRule class>>assignmentAndTesting
^self  createParseTreeRule: #(‘( `var := `@object ) `message’)
       name: ‘Assigns and tests an object’

Использует 1 / объект вместо #reciprocal

Это кажется лишним, но в Меркапе мы разрабатываем финансовое программное обеспечение, и у нас есть много математических абстракций, помимо чисел, таких как цены, котировки, денежные меры и т. Д. Поэтому проще отправить ответное сообщение математическому объекту чтобы получить обратную валюту, например, вместо того, чтобы делать 1 / (20.45 ARS/USD), мы делаем (20.45 ARS/USD) reciprocal, чтобы получить обратную котировку валюты. Когда у вас длинная финансовая формула, это обеспечивает лучшую читаемость.

ParseTreeLintRule class>>assignmentAndTesting
^self  createParseTreeRule: #('1 / `@object') 
       name: 'Uses 1 / object instead of #reciprocal'

Ошибки и возможные ошибки

Использует каскадирование при создании Array / OrderedCollection. Удалите лишний «;»

Это правило возникло из-за неудачного опыта преобразования некоторого #add: cascade в более простой метод send. Например токарная обработка:

OrderedCollection new 
  add:1;
  add:2;
  add:3;
  yourself

в

OrderedCollection with: 1 with: 2 with: 3

Часто случалось, что мы немного искали #add: затем заменяли его на #with:, забывая удалить ; и замечая его только тогда, когда позже какой-то тест терпит неудачу.

ParseTreeLintRule class>>avoidCascadingAnArrayCreation
^self createParseTreeRule: #(
  ‘Array with: `@firstObject ; with: `@secondObject’
  ‘Array with: `@firstObject ; with: `@secondObject ; with: `@thirdObject’
  ‘Array with: `@firstObject with: `@secondObject ; with: `@thirdObject’
  ‘Array with: `@firstObject ; with: `@secondObject with: `@thirdObject’
  ‘Array with: `@firstObject ; with: `@secondObject ; with: `@thirdObject ; with: `@fourthObject’
  ‘Array with: `@firstObject with: `@secondObject ; with: `@thirdObject ; with: `@fourthObject’
  ‘Array with: `@firstObject with: `@secondObject with: `@thirdObject ; with: `@fourthObject’
  ‘Array with: `@firstObject with: `@secondObject ; with: `@thirdObject with: `@fourthObject’
  ‘Array with: `@firstObject ; with: `@secondObject with: `@thirdObject ; with: `@fourthObject’
  ‘Array with: `@firstObject ; with: `@secondObject ; with: `@thirdObject with: `@fourthObject’
  ‘Array with: `@firstObject ; with: `@secondObject with: `@thirdObject with: `@fourthObject’
)
name: ‘Uses cascading in Array/OrderedCollection creation. Remove the extra “;”’

В предыдущем примере используется массив, но исходное правило немного сложнее, включая другие коллекции. Я упростил его здесь по дидактическим причинам.

Класс реализует #doesNotUnderstand: но пропускает #respondsTo:

Это правило покрывает возможную ошибку и проверяет, что если класс реализует #doesNotUnderstand:, вероятно, ему также необходимо реализовать #respondsTo:, потому что эти методы должны работать в тандеме. Мы не используем его часто, но были случаи, когда мы пропускали #respondsTo: реализацию и замечали ее слишком поздно.

BlockLintRule>>implementorsOfDoesNotUnderstandMustImplementRespondsTo
^self new 
  resultClass: ClassEnvironment;
  name: ‘Class implements #doesNotUnderstand: but misses #respondsTo:’;
  classBlock: [:context :result | | currentClass |
    currentClass := context selectedClass.
    ((currentClass includesSelector: #doesNotUnderstand:)
     and: [(currentClass includesSelector: #respondsTo:) not])
        ifTrue: [result addClass: currentClass]];
  yourself

Спасибо за чтение и не стесняйтесь делиться своими собственными правилами или практиками. Если вы еще не автоматизировали эти правила, начните прямо сейчас !.