NexJ Logo

Linting Scheme code in NexJ Studio projects

The NexJ linter mixin strictly enforces coding standards in Scheme code or scripts and XML source elements. A linter unit test collects lint violations and reports the violations as a failed test. The test takes a set of predefined linting rules and applies them to the source code found in a project's resources.

Because most projects have a mature code base containing many violations, the following linting approaches have been developed:

  • Collect the lint violations for all the project resources (by not specifying a base revision), or the allFiles mode or strategy. This approach is recommended for new projects.
  • Use a repository-aware approach to lint resources touched between a specified base revision, and the tip revision or touchedFiles strategy. Currently, Mercurial repositories support the retrieval of revisions, resources touched between revisions, and annotation of resources. The linter engine lints each touched resource and reports new violations based on an annotation of the resource, which allows the revision of the violation to be determined. If the revision is after the base revision, it is reported as a new violation. 

The linter engine requires access to the project's repository to retrieve the information. For example, if your source code is stored in Mercurial, the linter engine needs to access the hg.exe executable file.

Linting rules

Adding a new rule

To implement a new rule, create a new metaclass based on linter:Rule, the abstract class of linter rules. The linter engine finds and loads all concrete implementations of linter:Rule including any new rules created in the project. A new rule must implement the event collectLint(resource) or the event that returns the lint violations (instances of linter:Violation) found in a resource as defined by this new rule. The lint violation reports the resource with the violation, the source line number, a line number relative to the Scheme script (if defined), and the position or column of the violation.

The creation of a linter:Violation instance is controlled by the static event linter:Rule'checkLintRevision which is passed parameters to determine whether the violation is to be reported. An instance is always created with the allFiles strategy, but with the touchedFiles strategy, a check of the revision of the line of source code must be made against the base revision defined for the linter engine.

Opting out of a rule

In certain circumstances, it may be necessary to ignore linting rules. This can be done by disabling a rule, or by allowing the linter engine to ignore a resource or a type of resource, or by specifying a lint violation as acceptable.

  • Disable a rule - Rules may be disabled by setting the flag 'isEnabled to false. By default, all rules are enabled.
  • Ignore a specific resource - By default, all resources are linted. To stop a resource from linting, add the path to the static collection attribute linter:Resource'OPT_OUT_RESOURCES. For example, adding meta/classes/roadshow does not lint all the roadshow metaclasses.
  • Ignore a type of resource - A rule may apply to 'opt in' resources, and not apply to 'opt out' resources. By default, a rule lints all resources. If 'opt in' resources are defined, the rule only applies to these resources. If 'opt out' resources are defined, the rule applies to all resources and not the defined resources. Behavior for defined 'opt in' and 'opt out' resources is currently undefined.
  • Lint overrides - For Scheme code, a lint violation can be overridden by adding the comment '; linter-disable-line' above a line or in-line.
    For example, to allow a space after the open brace:

; linter-disable-line
( define abc "123")
or
( define abc "123")  ; linter-disable-line

Project resources

The linter checks for lint in metadata resources defined in a project, and does not lint base resources. The linter engine abstracts types of NexJ resources that contain code for lint enforcement. The following are currently defined for linting:

  • linter:resource:Metaclass: For metaclasses and aspects.
  • linter:resource:Presentation: For presentation-layer resources (Classic and AFL) which include layouts, dialogs, forms, screens and other resources that have UI and data actions with Scheme scripts.
  • UnitTests: For unit test resources.
  • Scheme libraries: For files consisting of one scheme script.

These resources implement the abstract event linter:Resource'addContainers and add instances of linter:container:Scheme to the resource for each container of Scheme code defined in the resource. Newly defined resources need to implement this event and override the main action to add all containers for the new resource.

Adding the linter mixin to a project

To implement the linter mixin to a project:

  1. Add the linter mixin as an environment mixin, to .metadata.

  2. Enable Show Environment Mixin Resources in NexJ Studio perspective to show the unit test linter:LintCollector.

    Internal or other linter unit tests only run in the Linter project.

Fixing reported violations

When the linter unit test fails, the violations are reported to the console. Each violation is reported with a description of the rule, the resource where it was found, the name of the container of the Scheme script (an action of a metaclass event), the line number and position.  Also, a goto line similar to a stack trace is available to allow a one-step click to take the developer to the violation.

Re-run the unit test until it passes.


Linting with the repository-aware approach requires that any outstanding changes be committed. If they are not, linting a resource (read from the project folder) and retrieving its annotation may not match in total number of lines. This can cause an error when the linter engine finds a violation in a line that does not exist in the copy retrieved with the annotation from the repository.

Troubleshooting

If you get the error "Cannot run program "hg" (in directory <dir>)" when you are executing the unit test, this indicates that the linter engine cannot find the hg.exe file required for projects with Mercurial repositories. Ensure that Mercurial is installed. 


Defined rules for the Linter engine

The following is a complete list of rules currently defined in the Linter engine:

RegEx Rules

Lint found by detection with a regular expression.

Rule NameDescription
Trailing White SpaceTrailing white space or spaces at the end of a line are not permitted.
TabsNo tabs are permitted, only spaces. In practice, a tab is usually denoted by 3 consecutive spaces.
Spaces Before Closing BraceSpaces before a closing brace are not permitted.
Spaces After Opening BraceSpaces after an opening brace are not permitted.
Single SpaceOnly single spaces are allowed.
New Line StartEmpty lines at the beginning of Scheme script are not permitted.
New Line EndEmpty lines at the end of Scheme script are not permitted.
Multiple New LinesMore than one consecutive empty line inside Scheme script is not permitted.

ElementXML Rules

Lint found in XML elements of a resource XML source code.

Rule NameDescription
Element DescriptionCheck for the existence of the description tag on XML elements. For example, metaclass attributes require descriptions.
Element PositionCheck the position of XML elements. For example, product metaclass attributes are to be added at the end of the <Attributes> list whereas attributes added as project-level customizations are to be added at the beginning.
Project Element DescriptionEnforce module name prefix in the description tag of XML elements for project-level customizations. For example, "UBS: This is an attribute description."
Project Element NameEnforce module name prefix in the name tag of XML elements for metaclass attributes and events for project-level customizations.
Public API PrivilegeEnforce that a metaclass event with public visibility has a defined privilege.
Static AttributeEnforce naming convention of static attributes on metaclasses using upper-case and underscores (eg. ADDRESS_DEFAULT_FIELDS).

Pattern Rules

Lint found by recursively checking Scheme script for coding 'patterns'.

Rule NameDescription
AmacrosLibrary macros need to be defined in Amacros.scm to be loaded first for use by other macros. Place macro in Amacros.scm.
Double ReadFinds nested read invocations, hinting at a potential performance issue. See Note 1 below.
If-ThenFinds (if ..) expressions without an associated "else" case. One should use the (when ..) expression as convention.
If-Then-ElseFinds (if ..) multi-lined expressions with an "else" case without an else comment separating the "then" and "else" case. One should place an else comment between the "then" and "else" case for readability.
When-NotFinds (when (not ..) ..) expressions. One should use the (unless ..) expression as convention.
LoggerInfoUsage of INFO level logger statements are not permitted.
LoopWithReadInstanceFinds read-instance expressions inside of loops such as for-each and map, which like Double Read can signify performance issues.
CommitInvocations of (commit) should be limited to unit tests. The use of (begin-transaction) is more appropriate in some cases.
NowInvocations of (now) should not appear in unit tests, because unit tests should be deterministic.
TodayInvocations of (today) should not appear in unit tests, because unit tests should be deterministic.
LetStarA let* statement is used when a let will suffice. The variable initializers do not reference previous variables.

Note 1: Double Read

The following is an example of a nested read that would be prevented by the Double Read rule:

(let
  (
    (test "ABC")
    (where1 '(= (@ lintMode) "touchedFiles"))
    (where2 '(def))
  )
  (for-each
    (lambda (entity)
      (read-instance
        linter:Linter
        ()
        '(= (@ lintMode) "touchedFiles")
        #f
      )
    )
    (linter:Linter'read () '(= (@ lintMode) "allFiles") () () () #f)
  )
)

In a similar manner, the Read Instance rule applies but much more generally to loops, even if it's not a nested read

(for-each
  (lambda (n)
    (read-instance
      linter:Linter
      ()
      '(= (@ lintMode) "touchedFiles")
      #f
    )
  )
  (range 10)
)