UNPKG

tslint-microsoft-contrib

Version:
294 lines 13.5 kB
"use strict"; 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