A TADS3 module for implementing linters

Here’s a simple module that’s intended to make it easier to implement linters: linter github repo.

You declare an instance of the Linter class, putting your static analysis tests in the lint() method.

You can use Linter.error() to add an error, and Linter.warning() to add a warning:

              // Declare a linter.
              myLinter: Linter
                      lint() {
                              warning('this is an example warning');
                              error('this is an example error');
                      }
              ;

The lint() method will be called during preinit in debugging builds (when t3make is run with the -d flag).

By default the linter will throw an exception and exit at runtime if there are any errors, outputting all errors and warnings.

If compiled with -D WERROR the linter will treat all warnings as errors.

If the -d flag is not used when compiling the module will do nothing.

There’s also one utility method on Linter: superclassListIsABeforeB(obj, cls0, cls1). It checks the object obj to see if class cls0 occurs before class cls1 in the object’s superclass list.

I’ll probably add a few more utility methods to the base module, but it’s mostly designed as a framework for hanging bespoke tests off of.

6 Likes

Added a couple additional utility methods, all wrappers around basic forEachInstance loops:

  • isSingleton(cls) Returns boolean true if there’s only one instance of the class cls
  • checkForOrphans(cls) Checks all instances of the class cls and returns boolean true if any instance’s location is nil
  • testInstanceProp(cls, fn) Iterates over all instances of the class cls. If fn is a method or property, the value of that method or property on each instance will be checked. If fn is a function it will be called with each instance as its argument. The return value is boolean true if the return value of the check above is true for all instances, nil otherwise

The usage for the first two should be obvious. For the last one, it means that:

testInstanceProp(Foo, &someMethod)

…will test each instance of Foo and will return true if someMethod() returns true for all of them. Alternately,

testInstanceProp(Bar, { x: x.someProp == 5 })

…will test each instance of Bar and will return true if the
value of someProp is exactly 5 on all of them.

3 Likes

I am going to for sure adopt this when I get back (1 week to end of Comp!). Will create (assuming Night Elves don’t fix my shoes first…) checks for checkObjInConsultable(objCls, cnslt) and checkObjBlockedByConnector(objCls, trvlCnct) for some game-specific checking right out of the gate.

2 Likes

Another update. I’m fiddling around with some ways to declare checks.

The module now provides two more classes, LintClass and LintRule.

LintClass can be used to apply a check to every instance of a class. Example:

              myLinter: Linter;
              +LintClass @Foo
                      lintAction(obj) {
                              if(obj.foo == 'bar')
                                      warning('foo is bar');
                      }
              ;

This will iterate through all instance of Foo, calling lintAction() for each instance, with the argument being the instance itself.

LintClass provides error(), warning(), and info(), which just call the same methods on the parent Linter.

LintClass also provides a setFlag() method. It takes a single text literal as its argument. Flags can then be checked via LintRule. Example:

              myLinter: Linter;
              +LintClass @Foo
                      lintAction(obj) {
                              if(obj.foo == 'bar') {
                                      warning('foo is bar');
                                      setFlag('fooIsBar');
                              }
                      }
              ;
              +LintClass @Bar
                      lintAction(obj) {
                              if(obj.bar == 'foo') {
                                      warning('bar is foo');
                                      setFlag('barIsFoo');
                              }
                      }
              ;
              +LintRule [ 'fooIsBar', 'barIsFoo' ]
                      lintAction() {
                              error('foo and bar potentially reversed');
                      }
              ;

This will check every instance of Foo to see if any instance’s foo property is 'bar'. It will also check every instance of Bar to see if any instance’s bar property is 'foo'. On any match a warning will be added and a flag will be set. The LintRule will then match when both flags are set, and log an error.

Both LintClass and LintRule checks are run after the Linter’s lint() method is evaluated, so lint() can set flags to be checked by LintRules.

This is just intended to (hopefully) make it easier to write checks.

1 Like