Suppress and extend TSLint linting errors, before they get returned to the console or your code editor.
Table of Contents
- Use Cases
- Installation
- Predefined rule wrappers
- Disable/enable rules by their original name in comment flags in source code
- Custom rule wrappers
Many TSLint rules are very limited by their configurability, and some rules looks like they are not thought to the end.
For example, I want to prevent the usage of "I" as prefix for interface names. The TSLint rule for that is called "interface-name".
Unfortunately, this rule also shows an error for "I18N", which is an absolutely valid interface name to me.
Or, in my React projects I want to get a linting error, if I forgot to specify a Components class method as private
or public
using the "member-access" rule. But for the React methods componentDidMount
, render
, getDerivedStateFromProps
etc. I don't want to specify that, because they are always public.
Unfortunately, by now, it's not possible to specify a whitelist here.
I want to prefer conditional expressions for small, simple alignments, but "prefer-conditional-expression" also complains about complex statements, which wouldn't be easy readable in a single line, because this line would have a size of 300 characters or more.
Why isn't there a way to show the linting error only, if the conditional expression would be a ...let's say... less-than-120-chars-one-liner?
Using TSLint-Filter, you have the possibility to easily extend existing rules and suppress specific linting errors, based on regular expressions.
It's even possible to use integer ranges in these regular expression, to filter by a range of numbers in the error message.
At this time (2019-01-17), the tslint-microsoft-contrib rule "import-name" throws an error for empty imports (imports of modules for side effects only) like
import './polyfill'
While TSLint doesn't handle such JavaScript errors, your code editor may suppress this error silently and may stop linting your whole project or at least the current file, so that you think your files are free of issues, because your editor doesn't show any.
TSLint-Filter catches such JavaScript errors and show them as normal linting errors for the first character of a file, so that you get visual feedback, that there's something wrong.
Using the filter ability of TSLint-Filter you are then also able to suppress the specific error, without to affect the execution of other rules.
Don't forget to report errors to rule authors, so that they are able to fix them!
Install with npm
npm install tslint-filter --save-dev
Since TSLint does not provide an easy way to modify linting errors before they get returned, we need to create own rules, with TSLint-Filter as wrapper for the original rule.
But that's very easy:
Either use one of the predefined rule wrappers, or create a custom rule wrapper.
The TSLint-Filter package contains a couple of predefined rule wrappers, which I'm using in my projects.
In your tslint.json
either add:
"extends": [
"tslint-filter"
]
or
"rulesDirectory": [
"node_modules/tslint-filter/rules"
]
Now, you can replace the original rule, by one of the following predefined rule wrappers:
Rule Name | Original Rule | Description |
---|---|---|
___deprecation | tslint » deprecation | Adds the context of the deprecated identifier to the message. Original message: which is deprecated. New message: KeyboardEvent.which is deprecated. |
___import-name | tslint-microsoft-contrib » import-name | Adds the full import path to the message. Original message: Misnamed import. Import should be named 'xyz' but found 'zyx' New message: Misnamed import. Import should be named 'xyz' but found 'zyx' for './my-modules/xyz' |
___interface-name | tslint » interface-name | Adds the criticized interface name to the message. Original message:> Interface name must not have an "I" prefix New message: Interface name "I18N" must not have an "I" prefix |
___match-default-export-name | tslint » match-default-export-name | Adds the full import path to the message. Original message: Expected import 'xyz' to match the default export 'zyx'. New message: Expected import 'xyz' of module './my-modules/xyz' to match the default export 'zyx'. |
___member-access | tslint » member-access | Nothing special. Just enables the ability to filter specific linting errors in the tslint.json . |
___object-literal-sort-keys | tslint » object-literal-sort-keys | Nothing special. Just enables the ability to filter specific linting errors in the tslint.json . |
___prefer-conditional-expression | tslint » prefer-conditional-expression | Adds an estimation of the saved characters, and the new size to the message. Original message: Use a conditional expression instead of assigning to 'myVar' in multiple places. New message: Use a conditional expression instead of assigning to 'myVar' in multiple places. (save about 36 characters, conditional expression size would be about 29 characters) |
___space-in-parens | tslint-eslint-rules » space-in-parens | Allow block comments inside of parentheses, like:import(/* webpackChunkName: "my-chunk-name" */'module'); |
___strict-boolean-expressions | tslint » strict-boolean-expressions | Adds the context of the expression to the message. Original message: This type is not allowed in the operand for the '&&' operator because it is always truthy. It may be null/undefined, but neither 'allow-null-union' nor 'allow-undefined-union' is set. Allowed types are boolean, enum, or boolean-or-undefined. New message: This type is not allowed in the operand for the '&&' operator in JsxExpression because it is always truthy. It may be null/undefined, but neither 'allow-null-union' nor 'allow-undefined-union' is set. Allowed types are boolean, enum, or boolean-or-undefined. |
___typedef | tslint » typedef | Nothing special. Just enables the ability to filter specific linting errors in the tslint.json . |
The configuration is equally to the original rule, expect that the last argument takes an array of regular expression. Like:
"___prefer-conditional-expression": [true, "check-else-if", [
"conditional expression size would be about [120...]"
]],
(see the topic ranges in regular expressions to read about the above regexp)
Even if you just use TSLint-Filter to modify the linting error message, you need to add an empty array, if the last argument of the rule is already an array. Otherwise, TSLint-Filter will misinterpret the rule argument as array of RegExp pattern to ignore.
TSLint allows you to enable or disable specific rules by their name directly in the source code, like
// tslint:disable-next-line:rule-name
Normally, the rule-name
is the name of the rule you use in your tslint.json
. That would mean, if you change interface-name
to ___interface-name
, you would also need to update all comments which are using this rule name.
To avoid that, TSLint-Filter pretend to have the name of the original rule, so you don't need to change anything.
First, create a folder for custom rules in your project folder.
In this folder create a JavaScript file like this:
module.exports = require('tslint-filter')('tslint/lib/rules/memberAccessRule');
"tslint/lib/rules/memberAccessRule" is the name of the original rule, which you want to extend.
You can name the file to whatever you want, but it must end with "Rule.js". I prefer to use the name of the original rule, and prefix it with "___" (3x underscore), so in this case "___memberAccessRule.js".
In your tslint.json
add the folder to the "rulesDirectory" section:
{
"rulesDirectory": [
"script/custom-tslint-rules"
],
"rules": {
Now, instead of using the rule "member-access", you're able to use the rule "___member-access".
If the last argument is an array it will be interpreted as an array of regular expressions. Linting errors which match these expressions will be ignored.
"___member-access": [true, [
"'(getDerivedStateFromProps|componentDidMount|shouldComponentUpdate|render|getSnapshotBeforeUpdate|componentDidUpdate|componentWillUnmount)'"
]],
Beside simply ignoring linting errors, you can also manipulate them. You can change the message, implement a fix or whatever you like.
Here is a simply starting point for own scripts:
const utils = require('tsutils');
module.exports = require('tslint-filter')('tslint/lib/rules/...', {
/**
* @param {import('tslint').RuleFailure} [failure]
* @param {import('typescript').SourceFile} [sourceFile]
* @param {ts.Program | undefined} [program]
*/
modifyFailure (failure, sourceFile, program) {
const node = utils.getTokenAtPosition(sourceFile, failure.getStartPosition().getPosition());
if (program) {
const checker = program.getTypeChecker();
// Work with types here ...
}
if (node.getText() === 'SomeText') {
// If no value is returned, the linting error get suppressed
return;
}
if (utils.isImportDeclaration(node.parent)) {
// If a string is returned, the original linting error message get changed
return `${failure.getFailure()} some more text.`;
}
// Keep the original linting error untouched. You could also create a new RuleFailure and return it
return failure;
}
});
For example, we want to extend the "interface-name" rule, to allow the interface name "I18N", even if it starts with "I".
Unfortunately, the message of this rule does not provide the name of the interface, so first, we have to include the name into the message:
const utils = require('tsutils');
module.exports = require('tslint-filter')('tslint-microsoft-contrib/importNameRule', {
/**
* @param {import('tslint').RuleFailure} [failure]
* @param {import('typescript').SourceFile} [sourceFile]
*/
modifyFailure (failure, sourceFile) {
if (/^Misnamed import\./.test(failure.getFailure())) {
const node = utils.getTokenAtPosition(sourceFile, failure.getStartPosition().getPosition());
if (utils.isImportDeclaration(node.parent) && utils.isLiteralExpression(node.parent.moduleSpecifier)) {
return `${failure.getFailure()} for '${node.parent.moduleSpecifier.text}'`;
}
}
return failure;
}
});
Now you can ignore interface names, starting with "I" followed by a digit:
"___interface-name": [true, "never-prefix", [
"Interface name \"I[\\d]"
]],
For example the "prefer-conditional-expression" rule could be extended to show the approximated number of characters you could save, and also the approximated size if you write the statement as conditional expression:
const utils = require('tsutils');
module.exports = require('tslint-filter')('tslint/lib/rules/preferConditionalExpressionRule', {
/**
* @param {import('tslint').RuleFailure} [failure]
* @param {import('typescript').SourceFile} [sourceFile]
*/
modifyFailure (failure, sourceFile) {
const match = failure.getFailure().match(/'([^\0]+)'/);
if (match !== null) {
const node = utils.getTokenAtPosition(sourceFile, failure.getStartPosition().getPosition()).parent;
if (utils.isIfStatement(node)) {
const originalSize = (node.end - node.pos);
const assigneeLength = match[1].length;
const expressionLength = node.expression.end - node.expression.pos;
const thenStatementLength = node.thenStatement.getText().replace(/^{?[\s\n]+|[\s\n]+}?$/g, '').length;
const elseStatementLength = node.elseStatement.getText().replace(/^{?[\s\n]+|[\s\n]+}?$/g, '').length;
// That's only an approximated size, depending on the wrapping characters
const newLength = expressionLength + thenStatementLength + elseStatementLength - assigneeLength + 1;
if (newLength > originalSize) {
return;
}
return `${failure.getFailure()} (save about ${originalSize - newLength} characters, conditional expression size would be about ${newLength} characters)`;
}
}
return failure;
}
});
Save this file under the name "___preferConditionalExpressionRule.js" in your custom rule folder.
Now you can use this pattern, to prevent linting errors, where the conditional expression size would be 120 characters or more:
"___prefer-conditional-expression": [true, "check-else-if", [
"conditional expression size would be about [120...]"
]],
Ranges can be specified with:
RegExp character sets | Meaning |
---|---|
[-5...5] |
Any integer number from -5 to 5 |
[...100] |
Any integer number from -999999999999999 to 100 |
[10...] |
Any integer number from 10 to 999999999999999 |
[...] |
Any integer number from -999999999999999 to 999999999999999, but if possible you should prefer -?\d+ |
-999999999999999
and 999999999999999
are required, because the expression is converted into a valid RegExp, and here we always need to specify a range.
These numbers are chosen because they are very near to Number.MIN_SAFE_INTEGER and Number.MAX_SAFE_INTEGER, but the RegExp representation is still very short.
It's an open to-do to determine the directory and rule file name automatically, based on the
rulesDirectory
, but I haven't found an easy way to do that yet.
So, for now, it's required to specify the whole path to the rule, instead of just using the rule name.
Rule Package | Directory | Number of rules* |
---|---|---|
tslint | tslint/lib/rules/ | 153 |
tslint-microsoft-contrib | tslint-microsoft-contrib/ | 93 |
tslint-sonarts | tslint-sonarts/lib/rules/ | 71 |
rxjs-tslint-rules | rxjs-tslint-rules/dist/rules/ | 41 |
tslint-eslint-rules | tslint-eslint-rules/dist/rules/ | 38 |
tslint-consistent-codestyle | tslint-consistent-codestyle/rules/ | 19 |
tslint-config-security | tslint-config-security/dist/rules/ | 16 |
tslint-immutable | tslint-immutable/rules/ | 16 |
tslint-misc-rules | tslint-misc-rules/rules/ | 15 |
tslint-react | tslint-react/rules/ | 15 |
tslint-clean-code | tslint-clean-code/dist/src/ | 12 |
tslint-stencil | tslint-stencil/rules/ | 7 |
vrsource-tslint-rules | vrsource-tslint-rules/rules/ | 7 |
rxjs-tslint | rxjs-tslint/ | 4 |
tslint-jasmine-rules | tslint-jasmine-rules/dist/ | 3 |
tslint-defocus | tslint-defocus/dist/ | 1 |
tslint-lines-between-class-members | tslint-lines-between-class-members/ | 1 |
tslint-no-unused-expression-chai | tslint-no-unused-expression-chai/rules/ | 1 |
tslint-origin-ordered-imports-rule | tslint-origin-ordered-imports-rule/dist/ | 1 |
tslint-plugin-prettier | tslint-plugin-prettier/rules/ | 1 |
* as of 2019-03-01. List ordered by number of rules.
Dashes in the file names are converted to camel-case, but leading and trailing dashes are kept. "Rule" is appended.
So, the rule name -ab-cd-ef-
is located in the file -abCdEf-Rule
.