lintR

lintR is a "lint" program to give advice on Racket programs. It consumes one or more student programs, parses them into an Abstract Syntax Tree (AST), and runs scripts against them. It collects information about the program to give feedback to the students or to assign marks.

lintR is intended to replace RST.

lintR is implemented using Scala.

Configuration

lintR is configured using HOCON (Human-Optimized Config Object Notation). HOCON is a superset of Json but with a number of very useful extensions:

  • a more forgiving syntax, including comments and multi-line strings
  • ability to import or include configuration from another file
  • ability to refer to other parts of the file, enabling more DRY solutions
  • ability to encode and use defaults
Coupled with the above, the Scala libraries that read it do pretty rigorous type-checking, catching many errors early on.

As of 2022-01-03 a typical config file looks like this:

{
language: "htdp-beginner",

include file("conf/functionsNeverAllowed.conf"),

unitTestDefault: {
tType: "rktUnitTest"
awardMarks: 2
category: "correct"
passedMsg: "This test passed"
failedMsg: "Uh-oh, this test failed"
}

attackThreshold-1: ${unitTestDefault}{
expr: "(attack-threshold 16612)"
expect: "8"
}

tasks: [
${attackThreshold-1},
${unitTestDefault}{
expr: "(attack-threshold 16612)"
expect: "8"
},
{
tType: "rktAllowedFunctions"
expectedFunctions: ["cond", "define", "+", "-", "add1"]
bannedFunctions: ["zero?"]
awardMarks: 0
deductMarks: 5
functionName: "attack-threshold"
},
{
tType: "rktIsRecursive"
functionName: "attack-threshold"
},
{
tType: "rktIsNotRecursive"
functionName: "attack-threshold"
awardMarks: 1.0
}
]
}

Data enclosed in braces represent an object. The fields of the object are named and then followed with a colon. So the config object has, at the moment, fields for the (Racket) language, disallowedFunctions (included from another file), and a list of tasks (note the [ ... ] syntax).

The HOCON object can contain "fields" that are not used in the lintR object. unitTestDefault and attackThreshold-1 fall into this category. Both are used to make the config file more DRY.

The list of tasks specifies what lintR does for each Racket file it processes. Each element in the task list has a type, specified by the tType field. That is followed by data needed to run that specific task. Types of tasks, the arguments they take and their default values are specified below.

HOCON idioms

The full specification for HOCON is pretty detailed, but the place to go for a full understanding. Here, however, are the idioms that have been found most useful for listR configurations.

Comments: Anything after `#` or `//` until the end of the line is a comment. Use them!

Multi-line strings: Content between triple quotes is uninterpreted, including newlines and whitespace.

File inclusion: The documentation is confusing because the examples show how to include the keyword "include" rather than showing how the include keyword actually works. An "include" can replace an object field (including the name). The example, above, has an example. The included file, "conf/functionsNeverAllowed", contains the following text:

disallowedFunctions: [
"=~",
"acos",
"angle",
...
]

It has the effect of adding a "disallowedFunctions" field to the config object with a list of strings as its argument.

It seems utterly reasonable to me that one should be able to write

disallowedFunctions: include file("conf/functionsNeverAlllowed")

but they didn't ask me and it's not allowed.

There are a number of obvious uses of this capability:

  • Split up the file into several pieces, each of which are developed by different people -- or just for organizational purposes.
  • Factor out common config, either for an assignment or for an entire course offereing. The example of disallowed functions would be such a use.
Reference previous definitions: Previously defined config data can be referenced elsewhere in the config file. This is done with the ${...} syntax. It can include "dot" notation to access fields within fields. For example, the following will initialize awardMarks to 2.
{   defaults: {     
      marks: 2   
    }      
    tasks: [     
      { ...       
        awardMarks: ${defaults.marks}     
      }]}

Merge objects: The merge feature is another way to set up defaults that is more compact. It makes use of referencing previous definitions. It's based on "concatenating" two objects and, if there are duplicate fields, taking the second one. For example, here a previously defined object (unitTestDefaults) is referenced and concatenated with an anonymous object that provides some overrides.  

 {  unitTestDefaults: {     
     tType: "rktUnitTest"
     awardMarks: 2     
     passedMsg: "This test passed"
     failedMsg: "Uh-oh, this test failed"   
   }

   tasks: [     
     ${unitTestDefaults}{
        category: "correct"
        awardMarks: 5
     }]}

Here, awardMarks is included in the defaults but is overridden to change the default 2 to 5. The category is not included in the defaults but is expected to be defined, so must be declared in the override.

Note that the concatenated objects cannot have newlines between them. The following does not work:

${unitTestDefaults} 
{ category: "correct" }


-- %USERSIG{ByronWeberBecker - 2022-01-03}%

Comments



Edit | Attach | Watch | Print version | History: r3 < r2 < r1 | Backlinks | Raw View | Raw edit | More topic actions
Topic revision: r3 - 2022-03-29 - ByronWeberBecker
 
This site is powered by the TWiki collaboration platform Powered by PerlCopyright © 2008-2022 by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding TWiki? Send feedback