Working with Rules

Sergey Chernyshev edited this page Feb 25, 2016 · 5 revisions

Each CSS Lint rule has two files: a source file in the src/rules directory and a test file in the tests/rules directory. The basic source code format for a rule is:

CSSLint.addRule({
    //rule information
    id: "rule-id",
    name: "Rule name",
    desc: "Short description of rule",
    url: "http://documentation/url.html",
    browsers: "Affected browsers",

    //initialization
    init: function(parser, reporter){
        var rule = this;

        //rule initialization
    }
});

Each rule is represented by a single object with several properties and one method. The properties are:

  • id - the rule ID. This must be unique across all rules and must also be the name of the JavaScript file. This rule ID is used to configure CSS Lint on the command line as well as used in JavaScript to configure which rules CSS Lint uses.
  • name - a human readable name for the rule. This is used in the web UI to describe the rule.
  • desc - a human readable description for the rule. This is used in the web UI to describe the rule as well as on the command line when all rules are listed out.
  • browsers - a simple string describing which browsers this rule applies to. By default, use "All". If something is specific to a browser, put that in, such as "IE6".

The one method is init(), which sets up the rule. This method is passed in two arguments, a CSS parser and a reporter object. The CSS parser is an instance of parserlib.css.Parser, which is part of the ParserLib open source library (http://github.com/nzakas/parser-lib/). The parser is event-driven, so you can add listeners for certain events as the CSS is being parsed. This allows you to inspect pieces of the CSS as they occur in the code. Refer to the ParserLib documentation for more information on the parser API and the various events that are available.

Quite simply, the basic pattern to follow is:

parser.addListener("property", function(event){

    var propertyName    = event.property.toString().toLowerCase(),
        value           = event.value.toString(),
        line            = event.line,
        col             = event.col;

    switch(propertyName){
        case "width":
            //do something
            break;

        case "height":
            //do something
            break;

        case "custom-property":
            reporter.warn("Woah! Why are you using custom-property?", line, col, rule);
            break;

    }
});

There are a few things to note about this event handler. First, it listens for the "property" event, which fires for each property-value pair inside of a CSS rule. The event object contains a property property that contains an object with the name of the property in the raw form along with its line and column location. There is also a value object that contains the value for the property. Every event object for every event also has a line and col property that gives you basic location information, typically the location of the first part of the pattern that fired the event.

The second object passed to init(), the reporter, allows you to publish information to the CSS Lint results. The main method you'll use is report(), which publishes a warning or error (depending on the configuration being used). This method accepts four arguments: a message to display, a line number, a column number, and the rule object itself. For example:

reporter.report("This is unexpected!", 1, 1, rule);

The line and column number can be accessed from the parser object events, and these help the CSS Lint UI to display the warnings correctly. The last argument, the rule, gives reference information to CSS Lint about the warning so that the UI can display appropriate descriptions of the warnings.

Important: Even though the reporter object has error() and warn() methods, these should not be used. Only use report() in your rules.

Rule Unit Tests

Each rule must have a set of unit tests submitted with it to be accepted. The test file is named the same as the source file but lives in tests/. For example, if your rule source file is src/rules/foo.js then your test file should be tests/rules/foo.js. The test file will automatically be built in with all of the other test files when you run ant.

For your rule, be sure to test:

  1. All instances that should be flagged as warnings.
  2. At least two patterns that should not be flagged as warnings.

The basic pattern for a rule unit test file is:

(function(){

    /*global YUITest, CSSLint*/
    var Assert = YUITest.Assert;

    YUITest.TestRunner.add(new YUITest.TestCase({

        name: "Your Rule Tests",

        "Sentence describing what should happen": function(){
            var result = CSSLint.verify("/* CSS String */", { "your-rule": 1 });
            //asserts
        }
    }));

})();

The test case name should reflect your rule's name. Then, each test should be named as as sentence describing what is being tested. Inside of the rule, call CSSLint.verify() on a sample CSS string and make sure that just your rule is turned on by passing it as an option. Then, do your asserts on the result of the verification to make sure your rule is working. Here's an example:

"Using 0px should result in one warning": function(){
    var result = CSSLint.verify("h1 { left: 0px; }", { "zero-units": 1 });
    Assert.areEqual(1, result.messages.length);
    Assert.areEqual("warning", result.messages[0].type);
    Assert.areEqual("Values of 0 shouldn't have units specified.", result.messages[0].message);
}

You should always check the number of messages as your first assert. This ensures that there aren't any more or less messages than you're expecting. Assuming there are no syntax errors, only your rule will produce messages. The second step is to ensure the type of message is correct. Almost all rules output warnings, so check that this true for each message. Lastly, test the actual message text to ensure it's delivering the correct message to the user.

When testing the cases where your rule should not produce a message, just check that the number of returned messages is zero. For example:

"Using 0 should not result in a warning": function(){
    var result = CSSLint.verify("h1 { left: 0; }", { "zero-units": 1 });
    Assert.areEqual(0, result.messages.length);
}

Provide as many unit tests as possible. Your pull request will never be turned down for having too many tests submitted with it!

General Rule Tests

CSS Lint includes some general rule tests that are run on all rules in the system to prevent common errors. Your rule must pass these tests in order to be accepted. These tests are run automatically when unit tests are run both on the command line and in the browser.

Rule writing tips

  • When looking at a property name, always normalize by transferring to lowercase before comparing. The parser always presents the property in the way it was originally in the code.
  • The property event can fire after a startrule event, but also after other events such as startfontface. This is because CSS properties are used in many places aside from regular CSS rules. They may be used in font-faces, animation keyframes, etc. Due to this, it's recommended:
    • If you're listening for the startrule event, also listen to startfontface, startpage, startpagemargin, and startkeyframerule.
    • If you're listening for the endrule event, also listen to endfontface, endpage, endpagemargin, and endkeyframerule.
  • Always write unit tests for your rule.

Submitting Your Rule

Once you've written a rule, you can decide whether the rule is generic enough to be included in CSS Lint or if it's specific to your own use case. If you decide to submit your rule via a pull request, there are some things to keep in mind:

  1. Rules must be accompanied by unit tests.
  2. Rules must work both in the browser and with the CLIs. We can't accept any rules that don't work in all environments.
  3. In your pull request include:
    1. The use case for the rule - what is it trying to prevent or flag?
    2. Which browsers are affected.