1 | "use strict";
|
2 | var __extends = (this && this.__extends) || (function () {
|
3 | var extendStatics = function (d, b) {
|
4 | extendStatics = Object.setPrototypeOf ||
|
5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
|
7 | return extendStatics(d, b);
|
8 | }
|
9 | return function (d, b) {
|
10 | extendStatics(d, b);
|
11 | function __() { this.constructor = d; }
|
12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
13 | };
|
14 | })();
|
15 | Object.defineProperty(exports, "__esModule", { value: true });
|
16 | var ts = require("typescript");
|
17 | var Lint = require("tslint");
|
18 | var tsutils = require("tsutils");
|
19 | var AstUtils_1 = require("./utils/AstUtils");
|
20 | var Scope_1 = require("./utils/Scope");
|
21 | var Utils_1 = require("./utils/Utils");
|
22 | var TypeGuard_1 = require("./utils/TypeGuard");
|
23 | var FAILURE_ANONYMOUS_LISTENER = 'A new instance of an anonymous method is passed as a JSX attribute: ';
|
24 | var FAILURE_DOUBLE_BIND = "A function is having its 'this' reference bound twice in the constructor: ";
|
25 | var FAILURE_UNBOUND_LISTENER = "A class method is passed as a JSX attribute without having the 'this' reference bound: ";
|
26 | var Rule = (function (_super) {
|
27 | __extends(Rule, _super);
|
28 | function Rule() {
|
29 | return _super !== null && _super.apply(this, arguments) || this;
|
30 | }
|
31 | Rule.prototype.apply = function (sourceFile) {
|
32 | if (sourceFile.languageVariant === ts.LanguageVariant.JSX) {
|
33 | return this.applyWithFunction(sourceFile, walk, this.parseOptions(this.getOptions()));
|
34 | }
|
35 | else {
|
36 | return [];
|
37 | }
|
38 | };
|
39 | Rule.prototype.parseOptions = function (options) {
|
40 | var parsed = {
|
41 | allowAnonymousListeners: false,
|
42 | allowedDecorators: new Set()
|
43 | };
|
44 | options.ruleArguments.forEach(function (opt) {
|
45 | if (TypeGuard_1.isObject(opt)) {
|
46 | parsed.allowAnonymousListeners = opt['allow-anonymous-listeners'] === true;
|
47 | if (opt['bind-decorators']) {
|
48 | var allowedDecorators = opt['bind-decorators'];
|
49 | if (!Array.isArray(allowedDecorators) || allowedDecorators.some(function (decorator) { return typeof decorator !== 'string'; })) {
|
50 | throw new Error('one or more members of bind-decorators is invalid, string required.');
|
51 | }
|
52 | parsed.allowedDecorators = new Set(allowedDecorators);
|
53 | }
|
54 | }
|
55 | });
|
56 | return parsed;
|
57 | };
|
58 | Rule.metadata = {
|
59 | ruleName: 'react-this-binding-issue',
|
60 | type: 'maintainability',
|
61 | description: 'When using React components you must be careful to correctly bind the `this` reference ' +
|
62 | 'on any methods that you pass off to child components as callbacks.',
|
63 | options: {
|
64 | type: 'object',
|
65 | properties: {
|
66 | 'allow-anonymous-listeners': {
|
67 | type: 'boolean'
|
68 | },
|
69 | 'bind-decorators': {
|
70 | type: 'list',
|
71 | listType: {
|
72 | anyOf: {
|
73 | type: 'string'
|
74 | }
|
75 | }
|
76 | }
|
77 | }
|
78 | },
|
79 | optionExamples: [true, [true, { 'bind-decorators': ['autobind'] }]],
|
80 | optionsDescription: '',
|
81 | typescriptOnly: true,
|
82 | issueClass: 'Non-SDL',
|
83 | issueType: 'Error',
|
84 | severity: 'Critical',
|
85 | level: 'Opportunity for Excellence',
|
86 | group: 'Correctness'
|
87 | };
|
88 | return Rule;
|
89 | }(Lint.Rules.AbstractRule));
|
90 | exports.Rule = Rule;
|
91 | function walk(ctx) {
|
92 | var boundListeners = new Set();
|
93 | var declaredMethods = new Set();
|
94 | var scope;
|
95 | function isMethodBoundWithDecorators(node, allowedDecorators) {
|
96 | if (!(allowedDecorators.size > 0 && node.decorators && node.decorators.length > 0)) {
|
97 | return false;
|
98 | }
|
99 | return node.decorators.some(function (decorator) {
|
100 | if (decorator.kind !== ts.SyntaxKind.Decorator) {
|
101 | return false;
|
102 | }
|
103 | var source = node.getSourceFile();
|
104 | var text = decorator.expression.getText(source);
|
105 | return ctx.options.allowedDecorators.has(text);
|
106 | });
|
107 | }
|
108 | function isAttributeAnonymousFunction(attributeLikeElement) {
|
109 | if (ctx.options.allowAnonymousListeners) {
|
110 | return false;
|
111 | }
|
112 | if (attributeLikeElement.kind === ts.SyntaxKind.JsxAttribute) {
|
113 | var attribute = attributeLikeElement;
|
114 | if (attribute.initializer !== undefined && attribute.initializer.kind === ts.SyntaxKind.JsxExpression) {
|
115 | return isExpressionAnonymousFunction(attribute.initializer.expression);
|
116 | }
|
117 | }
|
118 | return false;
|
119 | }
|
120 | function isExpressionAnonymousFunction(expression) {
|
121 | if (expression === undefined) {
|
122 | return false;
|
123 | }
|
124 | if (expression.kind === ts.SyntaxKind.ArrowFunction || expression.kind === ts.SyntaxKind.FunctionExpression) {
|
125 | return true;
|
126 | }
|
127 | if (expression.kind === ts.SyntaxKind.CallExpression) {
|
128 | var callExpression = expression;
|
129 | var functionName = AstUtils_1.AstUtils.getFunctionName(callExpression);
|
130 | if (functionName === 'bind') {
|
131 | return true;
|
132 | }
|
133 | }
|
134 | if (expression.kind === ts.SyntaxKind.Identifier && scope !== undefined) {
|
135 | var symbolText = expression.getText();
|
136 | return scope.isFunctionSymbol(symbolText);
|
137 | }
|
138 | return false;
|
139 | }
|
140 | function isUnboundListener(attributeLikeElement) {
|
141 | if (attributeLikeElement.kind === ts.SyntaxKind.JsxAttribute) {
|
142 | var attribute = attributeLikeElement;
|
143 | if (attribute.initializer !== undefined && attribute.initializer.kind === ts.SyntaxKind.JsxExpression) {
|
144 | var jsxExpression = attribute.initializer;
|
145 | if (jsxExpression.expression !== undefined && jsxExpression.expression.kind === ts.SyntaxKind.PropertyAccessExpression) {
|
146 | var propAccess = jsxExpression.expression;
|
147 | if (propAccess.expression.getText() === 'this') {
|
148 | var listenerText = propAccess.getText();
|
149 | if (declaredMethods.has(listenerText) && !boundListeners.has(listenerText)) {
|
150 | return true;
|
151 | }
|
152 | }
|
153 | }
|
154 | }
|
155 | }
|
156 | return false;
|
157 | }
|
158 | function getSelfBoundListeners(node) {
|
159 | var result = new Set();
|
160 | if (node.body !== undefined && node.body.statements !== undefined) {
|
161 | node.body.statements.forEach(function (statement) {
|
162 | if (statement.kind === ts.SyntaxKind.ExpressionStatement) {
|
163 | var expressionStatement = statement;
|
164 | var expression = expressionStatement.expression;
|
165 | if (expression.kind === ts.SyntaxKind.BinaryExpression) {
|
166 | var binaryExpression = expression;
|
167 | var operator = binaryExpression.operatorToken;
|
168 | if (operator.kind === ts.SyntaxKind.EqualsToken) {
|
169 | if (binaryExpression.left.kind === ts.SyntaxKind.PropertyAccessExpression) {
|
170 | var leftPropText = binaryExpression.left.getText();
|
171 | if (binaryExpression.right.kind === ts.SyntaxKind.CallExpression) {
|
172 | var callExpression = binaryExpression.right;
|
173 | if (AstUtils_1.AstUtils.getFunctionName(callExpression) === 'bind' &&
|
174 | callExpression.arguments !== undefined &&
|
175 | callExpression.arguments.length === 1 &&
|
176 | callExpression.arguments[0].getText() === 'this') {
|
177 | var rightPropText = AstUtils_1.AstUtils.getFunctionTarget(callExpression);
|
178 | if (leftPropText === rightPropText) {
|
179 | if (result.has(rightPropText)) {
|
180 | var start = binaryExpression.getStart();
|
181 | var width = binaryExpression.getWidth();
|
182 | var msg = FAILURE_DOUBLE_BIND + binaryExpression.getText();
|
183 | ctx.addFailureAt(start, width, msg);
|
184 | }
|
185 | result.add(rightPropText);
|
186 | }
|
187 | }
|
188 | }
|
189 | }
|
190 | }
|
191 | }
|
192 | }
|
193 | });
|
194 | }
|
195 | return result;
|
196 | }
|
197 | function visitJsxOpeningElement(node) {
|
198 | node.attributes.properties.forEach(function (attributeLikeElement) {
|
199 | if (isUnboundListener(attributeLikeElement)) {
|
200 | var attribute = attributeLikeElement;
|
201 | var jsxExpression = attribute.initializer;
|
202 | if (jsxExpression === undefined || jsxExpression.kind === ts.SyntaxKind.StringLiteral) {
|
203 | return;
|
204 | }
|
205 | var propAccess = jsxExpression.expression;
|
206 | var listenerText = propAccess.getText();
|
207 | if (declaredMethods.has(listenerText) && !boundListeners.has(listenerText)) {
|
208 | var start = propAccess.getStart();
|
209 | var widget = propAccess.getWidth();
|
210 | var message = FAILURE_UNBOUND_LISTENER + listenerText;
|
211 | ctx.addFailureAt(start, widget, message);
|
212 | }
|
213 | }
|
214 | else if (isAttributeAnonymousFunction(attributeLikeElement)) {
|
215 | var attribute = attributeLikeElement;
|
216 | var jsxExpression = attribute.initializer;
|
217 | if (jsxExpression === undefined || jsxExpression.kind === ts.SyntaxKind.StringLiteral) {
|
218 | return;
|
219 | }
|
220 | var expression = jsxExpression.expression;
|
221 | if (expression === undefined) {
|
222 | return;
|
223 | }
|
224 | var start = expression.getStart();
|
225 | var widget = expression.getWidth();
|
226 | var message = FAILURE_ANONYMOUS_LISTENER + Utils_1.Utils.trimTo(expression.getText(), 30);
|
227 | ctx.addFailureAt(start, widget, message);
|
228 | }
|
229 | });
|
230 | }
|
231 | function cb(node) {
|
232 | if (tsutils.isMethodDeclaration(node)) {
|
233 | if (isMethodBoundWithDecorators(node, ctx.options.allowedDecorators)) {
|
234 | boundListeners = boundListeners.add('this.' + node.name.getText());
|
235 | }
|
236 | scope = new Scope_1.Scope(undefined);
|
237 | ts.forEachChild(node, cb);
|
238 | scope = undefined;
|
239 | return;
|
240 | }
|
241 | if (tsutils.isArrowFunction(node)) {
|
242 | if (scope !== undefined) {
|
243 | scope = new Scope_1.Scope(scope);
|
244 | }
|
245 | ts.forEachChild(node, cb);
|
246 | if (scope !== undefined) {
|
247 | scope = scope.parent;
|
248 | }
|
249 | return;
|
250 | }
|
251 | if (tsutils.isFunctionExpression(node)) {
|
252 | if (scope !== undefined) {
|
253 | scope = new Scope_1.Scope(scope);
|
254 | }
|
255 | ts.forEachChild(node, cb);
|
256 | if (scope !== undefined) {
|
257 | scope = scope.parent;
|
258 | }
|
259 | return;
|
260 | }
|
261 | if (tsutils.isClassDeclaration(node)) {
|
262 | boundListeners = new Set();
|
263 | declaredMethods = new Set();
|
264 | AstUtils_1.AstUtils.getDeclaredMethodNames(node).forEach(function (methodName) {
|
265 | declaredMethods.add('this.' + methodName);
|
266 | });
|
267 | }
|
268 | else if (tsutils.isConstructorDeclaration(node)) {
|
269 | boundListeners = getSelfBoundListeners(node);
|
270 | }
|
271 | else if (tsutils.isJsxElement(node)) {
|
272 | visitJsxOpeningElement(node.openingElement);
|
273 | }
|
274 | else if (tsutils.isJsxSelfClosingElement(node)) {
|
275 | visitJsxOpeningElement(node);
|
276 | }
|
277 | else if (tsutils.isVariableDeclaration(node)) {
|
278 | if (scope !== undefined) {
|
279 | if (node.name.kind === ts.SyntaxKind.Identifier) {
|
280 | var variableName = node.name.text;
|
281 | if (isExpressionAnonymousFunction(node.initializer)) {
|
282 | scope.addFunctionSymbol(variableName);
|
283 | }
|
284 | }
|
285 | }
|
286 | }
|
287 | return ts.forEachChild(node, cb);
|
288 | }
|
289 | return ts.forEachChild(ctx.sourceFile, cb);
|
290 | }
|
291 |
|
\ | No newline at end of file |