Skip to content
This repository was archived by the owner on Mar 23, 2024. It is now read-only.

Commit 3f0e899

Browse files
committed
validateIndentation: exception to indentation rules for module pattern
Fixes #436 Closes gh-981
1 parent dbd2879 commit 3f0e899

File tree

4 files changed

+200
-12
lines changed

4 files changed

+200
-12
lines changed

lib/rules/validate-indentation.js

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
*/
8787

8888
var assert = require('assert');
89+
var utils = require('../utils');
8990

9091
var blockParents = [
9192
'IfStatement',
@@ -100,6 +101,15 @@ var blockParents = [
100101
'CatchClause',
101102
];
102103

104+
var indentableNodes = {
105+
BlockStatement: 'body',
106+
Program: 'body',
107+
ObjectExpression: 'properties',
108+
ArrayExpression: 'elements',
109+
SwitchStatement: 'cases',
110+
SwitchCase: 'consequent'
111+
};
112+
103113
module.exports = function() {};
104114

105115
module.exports.prototype = {
@@ -128,15 +138,7 @@ module.exports.prototype = {
128138
}
129139

130140
this._breakIndents = null;
131-
132-
this._indentableNodes = {
133-
BlockStatement: 'body',
134-
Program: 'body',
135-
ObjectExpression: 'properties',
136-
ArrayExpression: 'elements',
137-
SwitchStatement: 'cases',
138-
SwitchCase: 'consequent'
139-
};
141+
this._moduleIndents = null;
140142
},
141143

142144
getOptionName: function() {
@@ -327,12 +329,54 @@ module.exports.prototype = {
327329
});
328330
}
329331

332+
function setModuleBody(node) {
333+
if (node.body.length !== 1 || node.body[0].type !== 'ExpressionStatement' ||
334+
node.body[0].expression.type !== 'CallExpression') {
335+
return;
336+
}
337+
338+
var callExpression = node.body[0].expression;
339+
var callee = callExpression.callee;
340+
var callArgs = callExpression.arguments;
341+
var iffeFunction = utils.getFunctionNodeFromIIFE(callExpression);
342+
343+
if (iffeFunction) {
344+
if (callArgs.length === 1 && callArgs[0].type === 'FunctionExpression') {
345+
// detect UMD Shim, where the file body is the body of the factory,
346+
// which is the sole argument to the IIFE
347+
moduleBody = callArgs[0].body;
348+
} else {
349+
// full file IIFE
350+
moduleBody = iffeFunction.body;
351+
}
352+
}
353+
354+
// detect require/define
355+
if (callee.type === 'Identifier' && callee.name.match(/^(require|define)$/)) {
356+
// the define callback is the *first* functionExpression encountered,
357+
// as it can be the first, second, or third argument.
358+
callArgs.some(function(argument) {
359+
if (argument.type === 'FunctionExpression') {
360+
moduleBody = argument.body;
361+
return true;
362+
}
363+
});
364+
}
365+
366+
// set number of indents for modules by detecting
367+
// whether the first statement is indented or not
368+
if (moduleBody) {
369+
_this._moduleIndents = moduleBody.body[0].loc.start.column > 0 ? 1 : 0;
370+
}
371+
}
372+
330373
function generateIndentations() {
331374
file.iterateNodesByType('Program', function(node) {
332375
if (!isMultiline(node)) {
333376
return;
334377
}
335378

379+
setModuleBody(node);
336380
markChildren(node);
337381
});
338382

@@ -341,9 +385,11 @@ module.exports.prototype = {
341385
return;
342386
}
343387

388+
var indents = node === moduleBody ? _this._moduleIndents : 1;
389+
344390
markChildren(node);
345-
markPop(node, 1);
346-
markPush(getBlockNodeToPush(node), 1);
391+
markPop(node, indents);
392+
markPush(getBlockNodeToPush(node), indents);
347393
markEndCheck(node);
348394
});
349395

@@ -410,7 +456,8 @@ module.exports.prototype = {
410456

411457
var _this = this;
412458

413-
var indentableNodes = this._indentableNodes;
459+
var moduleBody;
460+
414461
var indentChar = this._indentChar;
415462
var indentSize = this._indentSize;
416463

lib/utils.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,35 @@ exports.isSnakeCased = function(name) {
146146
return SNAKE_CASE_RE.test(name);
147147
};
148148

149+
/**
150+
* Returns the function expression node if the provided node is an iffe,
151+
* other returns undefined.
152+
*
153+
* @param {Object} node
154+
* @return {?Object}
155+
*/
156+
exports.getFunctionNodeFromIIFE = function(node) {
157+
if (node.type !== 'CallExpression') {
158+
return null;
159+
}
160+
161+
var callee = node.callee;
162+
163+
if (callee.type === 'FunctionExpression') {
164+
return callee;
165+
}
166+
167+
if (callee.type === 'MemberExpression' &&
168+
callee.object.type === 'FunctionExpression' &&
169+
callee.property.type === 'Identifier' &&
170+
(callee.property.name === 'call' || callee.property.name === 'apply')
171+
) {
172+
return callee.object;
173+
}
174+
175+
return null;
176+
};
177+
149178
/**
150179
* Trims leading and trailing underscores
151180
*

test/rules/validate-indentation.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,67 @@ describe('rules/validate-indentation', function() {
150150
});
151151
});
152152

153+
describe('module pattern indentation', function() {
154+
beforeEach(function() {
155+
checker.configure({ validateIndentation: 4 });
156+
});
157+
158+
var cases = {
159+
'no indentation': 'a++;',
160+
indentation: ' a++;'
161+
};
162+
163+
Object.keys(cases).forEach(function(title) {
164+
var statement = cases[title];
165+
166+
it('should allow ' + title + ' in UMD Shim', function() {
167+
var source = '\n' +
168+
'(function( factory ) {\n' +
169+
' if ( typeof define === "function" && define.amd ) {\n' +
170+
' define(factory);\n' +
171+
' } else {\n' +
172+
' factory();\n' +
173+
' }\n' +
174+
'}(function( $ ) {\n' +
175+
statement + '\n' +
176+
'}));';
177+
assert(checker.checkString(source).isEmpty());
178+
});
179+
180+
it('should allow ' + title + ' in define', function() {
181+
var source = '\n' +
182+
'define(["dep"], function( dep ) {\n' +
183+
statement + '\n' +
184+
'});';
185+
assert(checker.checkString(source).isEmpty());
186+
});
187+
188+
it('should allow ' + title + ' in require', function() {
189+
var source = '\n' +
190+
'require(["dep"], function( dep ) {\n' +
191+
statement + '\n' +
192+
'});';
193+
assert(checker.checkString(source).isEmpty());
194+
});
195+
196+
it('should allow ' + title + ' in full file IIFE', function() {
197+
var source = '\n' +
198+
'(function(global) {\n' +
199+
statement + '\n' +
200+
'}(this));';
201+
assert(checker.checkString(source).isEmpty());
202+
});
203+
});
204+
205+
it('should not allow no indentation in some other top level function', function() {
206+
var source = '\n' +
207+
'defines(["dep"], function( dep ) {\n' +
208+
'a++;\n' +
209+
'});';
210+
assert(!checker.checkString(source).isEmpty());
211+
});
212+
});
213+
153214
describe('switch identation', function() {
154215
beforeEach(function() {
155216
checker.configure({ validateIndentation: 4 });

test/utils.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
var utils = require('../lib/utils');
22
var assert = require('assert');
3+
var JsFile = require('../lib/js-file');
4+
var esprima = require('esprima');
35

46
describe('modules/utils', function() {
7+
8+
function createJsFile(source) {
9+
return new JsFile(
10+
'example.js',
11+
source,
12+
esprima.parse(source, {loc: true, range: true, comment: true, tokens: true})
13+
);
14+
}
15+
516
describe('isEs3Keyword', function() {
617
it('should return true for ES3 keywords', function() {
718
assert(utils.isEs3Keyword('break'));
@@ -60,6 +71,46 @@ describe('modules/utils', function() {
6071
});
6172
});
6273

74+
describe('getFunctionNodeFromIIFE', function() {
75+
it('should return the function from simple IIFE', function() {
76+
var file = createJsFile('var a = function(){a++;}();');
77+
var callExpression = file.getNodesByType('CallExpression')[0];
78+
var functionExpression = file.getNodesByType('FunctionExpression')[0];
79+
80+
assert.equal(utils.getFunctionNodeFromIIFE(callExpression), functionExpression);
81+
});
82+
83+
it('should return the function from call()\'ed IIFE', function() {
84+
var file = createJsFile('var a = function(){a++;}.call();');
85+
var callExpression = file.getNodesByType('CallExpression')[0];
86+
var functionExpression = file.getNodesByType('FunctionExpression')[0];
87+
88+
assert.equal(utils.getFunctionNodeFromIIFE(callExpression), functionExpression);
89+
});
90+
91+
it('should return the function from apply()\'ed IIFE', function() {
92+
var file = createJsFile('var a = function(){a++;}.apply();');
93+
var callExpression = file.getNodesByType('CallExpression')[0];
94+
var functionExpression = file.getNodesByType('FunctionExpression')[0];
95+
96+
assert.equal(utils.getFunctionNodeFromIIFE(callExpression), functionExpression);
97+
});
98+
99+
it('should return undefined for non callExpressions', function() {
100+
var file = createJsFile('var a = 1;');
101+
var notCallExpression = file.getNodesByType('VariableDeclaration')[0];
102+
103+
assert.equal(utils.getFunctionNodeFromIIFE(notCallExpression), undefined);
104+
});
105+
106+
it('should return undefined for normal function calls', function() {
107+
var file = createJsFile('call();');
108+
var callExpression = file.getNodesByType('CallExpression')[0];
109+
110+
assert.equal(utils.getFunctionNodeFromIIFE(callExpression), undefined);
111+
});
112+
});
113+
63114
describe('trimUnderscores', function() {
64115
it('should trim trailing underscores', function() {
65116
assert.equal(utils.trimUnderscores('__snake_cased'), 'snake_cased');

0 commit comments

Comments
 (0)