tslint-microsoft-contrib
Version:
TSLint Rules for Microsoft
294 lines • 13.5 kB
JavaScript
;
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
}
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
var ts = require("typescript");
var Lint = require("tslint");
var tsutils = require("tsutils");
var AstUtils_1 = require("./utils/AstUtils");
var Scope_1 = require("./utils/Scope");
var Utils_1 = require("./utils/Utils");
var TypeGuard_1 = require("./utils/TypeGuard");
var FAILURE_ANONYMOUS_LISTENER = 'A new instance of an anonymous method is passed as a JSX attribute: ';
var FAILURE_DOUBLE_BIND = "A function is having its 'this' reference bound twice in the constructor: ";
var FAILURE_UNBOUND_LISTENER = "A class method is passed as a JSX attribute without having the 'this' reference bound: ";
var Rule = (function (_super) {
__extends(Rule, _super);
function Rule() {
return _super !== null && _super.apply(this, arguments) || this;
}
Rule.prototype.apply = function (sourceFile) {
if (sourceFile.languageVariant === ts.LanguageVariant.JSX) {
return this.applyWithFunction(sourceFile, walk, this.parseOptions(this.getOptions()));
}
return [];
};
Rule.prototype.parseOptions = function (options) {
var parsed = {
allowAnonymousListeners: false,
allowedDecorators: new Set()
};
options.ruleArguments.forEach(function (opt) {
if (TypeGuard_1.isObject(opt)) {
parsed.allowAnonymousListeners = opt['allow-anonymous-listeners'] === true;
if (opt['bind-decorators']) {
var allowedDecorators = opt['bind-decorators'];
if (!Array.isArray(allowedDecorators) || allowedDecorators.some(function (decorator) { return typeof decorator !== 'string'; })) {
throw new Error('one or more members of bind-decorators is invalid, string required.');
}
parsed.allowedDecorators = new Set(allowedDecorators);
}
}
});
return parsed;
};
Rule.metadata = {
ruleName: 'react-this-binding-issue',
type: 'maintainability',
description: 'When using React components you must be careful to correctly bind the `this` reference ' +
'on any methods that you pass off to child components as callbacks.',
options: {
type: 'object',
properties: {
'allow-anonymous-listeners': {
type: 'boolean'
},
'bind-decorators': {
type: 'list',
listType: {
anyOf: {
type: 'string'
}
}
}
}
},
optionExamples: [true, [true, { 'bind-decorators': ['autobind'] }]],
optionsDescription: '',
typescriptOnly: true,
issueClass: 'Non-SDL',
issueType: 'Error',
severity: 'Critical',
level: 'Opportunity for Excellence',
group: 'Correctness'
};
return Rule;
}(Lint.Rules.AbstractRule));
exports.Rule = Rule;
function walk(ctx) {
var boundListeners = new Set();
var declaredMethods = new Set();
var scope;
function isMethodBoundWithDecorators(node, allowedDecorators) {
if (!(allowedDecorators.size > 0 && node.decorators && node.decorators.length > 0)) {
return false;
}
return node.decorators.some(function (decorator) {
if (decorator.kind !== ts.SyntaxKind.Decorator) {
return false;
}
var source = node.getSourceFile();
var text = decorator.expression.getText(source);
return ctx.options.allowedDecorators.has(text);
});
}
function isAttributeAnonymousFunction(attributeLikeElement) {
if (ctx.options.allowAnonymousListeners) {
return false;
}
if (attributeLikeElement.kind === ts.SyntaxKind.JsxAttribute) {
var attribute = attributeLikeElement;
if (attribute.initializer !== undefined && attribute.initializer.kind === ts.SyntaxKind.JsxExpression) {
return isExpressionAnonymousFunction(attribute.initializer.expression);
}
}
return false;
}
function isExpressionAnonymousFunction(expression) {
if (expression === undefined) {
return false;
}
if (expression.kind === ts.SyntaxKind.ArrowFunction || expression.kind === ts.SyntaxKind.FunctionExpression) {
return true;
}
if (expression.kind === ts.SyntaxKind.CallExpression) {
var callExpression = expression;
var functionName = AstUtils_1.AstUtils.getFunctionName(callExpression);
if (functionName === 'bind') {
return true;
}
}
if (expression.kind === ts.SyntaxKind.Identifier && scope !== undefined) {
var symbolText = expression.getText();
return scope.isFunctionSymbol(symbolText);
}
return false;
}
function isUnboundListener(attributeLikeElement) {
if (attributeLikeElement.kind === ts.SyntaxKind.JsxAttribute) {
var attribute = attributeLikeElement;
if (attribute.initializer !== undefined && attribute.initializer.kind === ts.SyntaxKind.JsxExpression) {
var jsxExpression = attribute.initializer;
if (jsxExpression.expression !== undefined && jsxExpression.expression.kind === ts.SyntaxKind.PropertyAccessExpression) {
var propAccess = jsxExpression.expression;
if (propAccess.expression.getText() === 'this') {
var listenerText = propAccess.getText();
if (declaredMethods.has(listenerText) && !boundListeners.has(listenerText)) {
return true;
}
}
}
}
}
return false;
}
function getSelfBoundListeners(node) {
var result = new Set();
if (node.body !== undefined && node.body.statements !== undefined) {
node.body.statements.forEach(function (statement) {
if (statement.kind === ts.SyntaxKind.ExpressionStatement) {
var expressionStatement = statement;
var expression = expressionStatement.expression;
if (expression.kind === ts.SyntaxKind.BinaryExpression) {
var binaryExpression = expression;
var operator = binaryExpression.operatorToken;
if (operator.kind === ts.SyntaxKind.EqualsToken) {
if (binaryExpression.left.kind === ts.SyntaxKind.PropertyAccessExpression) {
var leftPropText = binaryExpression.left.getText();
if (binaryExpression.right.kind === ts.SyntaxKind.CallExpression) {
var callExpression = binaryExpression.right;
if (AstUtils_1.AstUtils.getFunctionName(callExpression) === 'bind' &&
callExpression.arguments !== undefined &&
callExpression.arguments.length === 1 &&
callExpression.arguments[0].getText() === 'this') {
var rightPropText = AstUtils_1.AstUtils.getFunctionTarget(callExpression);
if (leftPropText === rightPropText) {
if (result.has(rightPropText)) {
var start = binaryExpression.getStart();
var width = binaryExpression.getWidth();
var msg = FAILURE_DOUBLE_BIND + binaryExpression.getText();
ctx.addFailureAt(start, width, msg);
}
result.add(rightPropText);
}
}
}
}
}
}
}
});
}
return result;
}
function visitJsxOpeningElement(node) {
node.attributes.properties.forEach(function (attributeLikeElement) {
if (isUnboundListener(attributeLikeElement)) {
var attribute = attributeLikeElement;
var jsxExpression = attribute.initializer;
if (jsxExpression === undefined || jsxExpression.kind === ts.SyntaxKind.StringLiteral) {
return;
}
var propAccess = jsxExpression.expression;
var listenerText = propAccess.getText();
if (declaredMethods.has(listenerText) && !boundListeners.has(listenerText)) {
var start = propAccess.getStart();
var widget = propAccess.getWidth();
var message = FAILURE_UNBOUND_LISTENER + listenerText;
ctx.addFailureAt(start, widget, message);
}
}
else if (isAttributeAnonymousFunction(attributeLikeElement)) {
var attribute = attributeLikeElement;
var jsxExpression = attribute.initializer;
if (jsxExpression === undefined || jsxExpression.kind === ts.SyntaxKind.StringLiteral) {
return;
}
var expression = jsxExpression.expression;
if (expression === undefined) {
return;
}
var start = expression.getStart();
var widget = expression.getWidth();
var message = FAILURE_ANONYMOUS_LISTENER + Utils_1.Utils.trimTo(expression.getText(), 30);
ctx.addFailureAt(start, widget, message);
}
});
}
function visitClassMember(node) {
if (tsutils.isConstructorDeclaration(node)) {
boundListeners = getSelfBoundListeners(node);
}
else if (tsutils.isMethodDeclaration(node)) {
if (isMethodBoundWithDecorators(node, ctx.options.allowedDecorators)) {
boundListeners = boundListeners.add('this.' + node.name.getText());
}
}
}
function cb(node) {
if (tsutils.isMethodDeclaration(node)) {
scope = new Scope_1.Scope(undefined);
ts.forEachChild(node, cb);
scope = undefined;
return;
}
if (tsutils.isArrowFunction(node)) {
if (scope !== undefined) {
scope = new Scope_1.Scope(scope);
}
ts.forEachChild(node, cb);
if (scope !== undefined) {
scope = scope.parent;
}
return;
}
if (tsutils.isFunctionExpression(node)) {
if (scope !== undefined) {
scope = new Scope_1.Scope(scope);
}
ts.forEachChild(node, cb);
if (scope !== undefined) {
scope = scope.parent;
}
return;
}
if (tsutils.isClassDeclaration(node)) {
boundListeners = new Set();
declaredMethods = new Set();
AstUtils_1.AstUtils.getDeclaredMethodNames(node).forEach(function (methodName) {
declaredMethods.add('this.' + methodName);
});
node.members.forEach(visitClassMember);
}
else if (tsutils.isJsxElement(node)) {
visitJsxOpeningElement(node.openingElement);
}
else if (tsutils.isJsxSelfClosingElement(node)) {
visitJsxOpeningElement(node);
}
else if (tsutils.isVariableDeclaration(node)) {
if (scope !== undefined) {
if (node.name.kind === ts.SyntaxKind.Identifier) {
var variableName = node.name.text;
if (isExpressionAnonymousFunction(node.initializer)) {
scope.addFunctionSymbol(variableName);
}
}
}
}
return ts.forEachChild(node, cb);
}
return ts.forEachChild(ctx.sourceFile, cb);
}
//# sourceMappingURL=reactThisBindingIssueRule.js.map