Skip to content

Commit 45ae014

Browse files
authored
Refactor lookupFiles and files (#3722)
* Extract `lookupFile` conditions into functions. * Rename functions/variables to match intent; various scope reductions * Ordered requires (node->third party->project). * Add/Correct JSDoc for various functions. * Replaced `hasMatchingExtname` implementation, providing ~3.5x speedup.
1 parent 94c9320 commit 45ae014

File tree

1 file changed

+114
-44
lines changed

1 file changed

+114
-44
lines changed

lib/utils.js

+114-44
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,27 @@
99
* Module dependencies.
1010
*/
1111

12-
var debug = require('debug')('mocha:watch');
1312
var fs = require('fs');
14-
var glob = require('glob');
1513
var path = require('path');
1614
var util = require('util');
17-
var join = path.join;
15+
var glob = require('glob');
1816
var he = require('he');
1917
var errors = require('./errors');
2018
var createNoFilesMatchPatternError = errors.createNoFilesMatchPatternError;
2119
var createMissingArgumentError = errors.createMissingArgumentError;
2220

21+
var assign = (exports.assign = require('object.assign').getPolyfill());
22+
2323
/**
24-
* Ignored directories.
24+
* Inherit the prototype methods from one constructor into another.
25+
*
26+
* @param {function} ctor - Constructor function which needs to inherit the
27+
* prototype.
28+
* @param {function} superCtor - Constructor function to inherit prototype from.
29+
* @throws {TypeError} if either constructor is null, or if super constructor
30+
* lacks a prototype.
2531
*/
26-
27-
var ignore = ['node_modules', '.git'];
28-
29-
exports.inherits = require('util').inherits;
30-
31-
var assign = (exports.assign = require('object.assign').getPolyfill());
32+
exports.inherits = util.inherits;
3233

3334
/**
3435
* Escape special characters in the given string of html.
@@ -62,6 +63,7 @@ exports.isString = function(obj) {
6263
*/
6364
exports.watch = function(files, fn) {
6465
var options = {interval: 100};
66+
var debug = require('debug')('mocha:watch');
6567
files.forEach(function(file) {
6668
debug('file %s', file);
6769
fs.watchFile(file, options, function(curr, prev) {
@@ -73,14 +75,25 @@ exports.watch = function(files, fn) {
7375
};
7476

7577
/**
76-
* Ignored files.
78+
* Predicate to screen `pathname` for further consideration.
79+
*
80+
* @description
81+
* Returns <code>false</code> for pathname referencing:
82+
* <ul>
83+
* <li>'npm' package installation directory
84+
* <li>'git' version control directory
85+
* </ul>
7786
*
7887
* @private
79-
* @param {string} path
80-
* @return {boolean}
88+
* @param {string} pathname - File or directory name to screen
89+
* @return {boolean} whether pathname should be further considered
90+
* @example
91+
* ['node_modules', 'test.js'].filter(considerFurther); // => ['test.js']
8192
*/
82-
function ignored(path) {
83-
return !~ignore.indexOf(path);
93+
function considerFurther(pathname) {
94+
var ignore = ['node_modules', '.git'];
95+
96+
return !~ignore.indexOf(pathname);
8497
}
8598

8699
/**
@@ -92,24 +105,22 @@ function ignored(path) {
92105
*
93106
* @private
94107
* @param {string} dir
95-
* @param {string[]} [ext=['.js']]
108+
* @param {string[]} [exts=['js']]
96109
* @param {Array} [ret=[]]
97110
* @return {Array}
98111
*/
99-
exports.files = function(dir, ext, ret) {
112+
exports.files = function(dir, exts, ret) {
100113
ret = ret || [];
101-
ext = ext || ['js'];
102-
103-
var re = new RegExp('\\.(' + ext.join('|') + ')$');
114+
exts = exts || ['js'];
104115

105116
fs.readdirSync(dir)
106-
.filter(ignored)
107-
.forEach(function(path) {
108-
path = join(dir, path);
109-
if (fs.lstatSync(path).isDirectory()) {
110-
exports.files(path, ext, ret);
111-
} else if (path.match(re)) {
112-
ret.push(path);
117+
.filter(considerFurther)
118+
.forEach(function(dirent) {
119+
var pathname = path.join(dir, dirent);
120+
if (fs.lstatSync(pathname).isDirectory()) {
121+
exports.files(pathname, exts, ret);
122+
} else if (hasMatchingExtname(pathname, exts)) {
123+
ret.push(pathname);
113124
}
114125
});
115126

@@ -506,6 +517,42 @@ exports.canonicalize = function canonicalize(value, stack, typeHint) {
506517
return canonicalizedObj;
507518
};
508519

520+
/**
521+
* Determines if pathname has a matching file extension.
522+
*
523+
* @private
524+
* @param {string} pathname - Pathname to check for match.
525+
* @param {string[]} exts - List of file extensions (sans period).
526+
* @return {boolean} whether file extension matches.
527+
* @example
528+
* hasMatchingExtname('foo.html', ['js', 'css']); // => false
529+
*/
530+
function hasMatchingExtname(pathname, exts) {
531+
var suffix = path.extname(pathname).slice(1);
532+
return exts.some(function(element) {
533+
return suffix === element;
534+
});
535+
}
536+
537+
/**
538+
* Determines if pathname would be a "hidden" file (or directory) on UN*X.
539+
*
540+
* @description
541+
* On UN*X, pathnames beginning with a full stop (aka dot) are hidden during
542+
* typical usage. Dotfiles, plain-text configuration files, are prime examples.
543+
*
544+
* @see {@link http://xahlee.info/UnixResource_dir/writ/unix_origin_of_dot_filename.html|Origin of Dot File Names}
545+
*
546+
* @private
547+
* @param {string} pathname - Pathname to check for match.
548+
* @return {boolean} whether pathname would be considered a hidden file.
549+
* @example
550+
* isHiddenOnUnix('.profile'); // => true
551+
*/
552+
function isHiddenOnUnix(pathname) {
553+
return path.basename(pathname)[0] === '.';
554+
}
555+
509556
/**
510557
* Lookup file names at the given `path`.
511558
*
@@ -520,14 +567,18 @@ exports.canonicalize = function canonicalize(value, stack, typeHint) {
520567
* @param {string[]} extensions - File extensions to look for.
521568
* @param {boolean} recursive - Whether to recurse into subdirectories.
522569
* @return {string[]} An array of paths.
570+
* @throws {Error} if no files match pattern.
571+
* @throws {TypeError} if `filepath` is directory and `extensions` not provided.
523572
*/
524573
exports.lookupFiles = function lookupFiles(filepath, extensions, recursive) {
525574
var files = [];
575+
var stat;
526576

527577
if (!fs.existsSync(filepath)) {
528578
if (fs.existsSync(filepath + '.js')) {
529579
filepath += '.js';
530580
} else {
581+
// Handle glob
531582
files = glob.sync(filepath);
532583
if (!files.length) {
533584
throw createNoFilesMatchPatternError(
@@ -539,8 +590,9 @@ exports.lookupFiles = function lookupFiles(filepath, extensions, recursive) {
539590
}
540591
}
541592

593+
// Handle file
542594
try {
543-
var stat = fs.statSync(filepath);
595+
stat = fs.statSync(filepath);
544596
if (stat.isFile()) {
545597
return filepath;
546598
}
@@ -549,13 +601,16 @@ exports.lookupFiles = function lookupFiles(filepath, extensions, recursive) {
549601
return;
550602
}
551603

552-
fs.readdirSync(filepath).forEach(function(file) {
553-
file = path.join(filepath, file);
604+
// Handle directory
605+
fs.readdirSync(filepath).forEach(function(dirent) {
606+
var pathname = path.join(filepath, dirent);
607+
var stat;
608+
554609
try {
555-
var stat = fs.statSync(file);
610+
stat = fs.statSync(pathname);
556611
if (stat.isDirectory()) {
557612
if (recursive) {
558-
files = files.concat(lookupFiles(file, extensions, recursive));
613+
files = files.concat(lookupFiles(pathname, extensions, recursive));
559614
}
560615
return;
561616
}
@@ -574,11 +629,15 @@ exports.lookupFiles = function lookupFiles(filepath, extensions, recursive) {
574629
'array'
575630
);
576631
}
577-
var re = new RegExp('\\.(?:' + extensions.join('|') + ')$');
578-
if (!stat.isFile() || !re.test(file) || path.basename(file)[0] === '.') {
632+
633+
if (
634+
!stat.isFile() ||
635+
!hasMatchingExtname(pathname, extensions) ||
636+
isHiddenOnUnix(pathname)
637+
) {
579638
return;
580639
}
581-
files.push(file);
640+
files.push(pathname);
582641
});
583642

584643
return files;
@@ -797,14 +856,19 @@ exports.ngettext = function(n, msg1, msg2) {
797856
exports.noop = function() {};
798857

799858
/**
800-
* @summary Creates a map-like object.
801-
* @desc A "map" is an object with no prototype, for our purposes. In some cases this would be more appropriate than a `Map`, especially if your environment doesn't support it. Recommended for use in Mocha's public APIs.
802-
* @param {...*} [obj] - Arguments to `Object.assign()`
803-
* @returns {Object} An object with no prototype, having `...obj` properties
859+
* Creates a map-like object.
860+
*
861+
* @description
862+
* A "map" is an object with no prototype, for our purposes. In some cases
863+
* this would be more appropriate than a `Map`, especially if your environment
864+
* doesn't support it. Recommended for use in Mocha's public APIs.
865+
*
804866
* @public
805-
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
806-
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create#Custom_and_Null_objects
807-
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
867+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map|MDN:Map}
868+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create#Custom_and_Null_objects|MDN:Object.create - Custom objects}
869+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign|MDN:Object.assign}
870+
* @param {...*} [obj] - Arguments to `Object.assign()`.
871+
* @returns {Object} An object with no prototype, having `...obj` properties
808872
*/
809873
exports.createMap = function(obj) {
810874
return assign.apply(
@@ -814,10 +878,16 @@ exports.createMap = function(obj) {
814878
};
815879

816880
/**
817-
* @summary Create a read-only map-like object.
818-
* This differs from {@link module:utils.createMap createMap} only in that the argument must be non-empty, because the result is frozen.
881+
* Creates a read-only map-like object.
882+
*
883+
* @description
884+
* This differs from {@link module:utils.createMap createMap} only in that
885+
* the argument must be non-empty, because the result is frozen.
886+
*
819887
* @see {@link module:utils.createMap createMap}
888+
* @param {...*} [obj] - Arguments to `Object.assign()`.
820889
* @returns {Object} A frozen object with no prototype, having `...obj` properties
890+
* @throws {TypeError} if argument is not a non-empty object.
821891
*/
822892
exports.defineConstants = function(obj) {
823893
if (type(obj) !== 'object' || !Object.keys(obj).length) {

0 commit comments

Comments
 (0)