Skip to content

More robust logic for various edge cases #29

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@

"rules": {
"id-length": [2, { "min": 1, "max": 27 }],
"no-magic-numbers": ["warn", { "ignore": [0, 1, -1] }],
},
}
48 changes: 42 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,53 @@
// @ts-check

'use strict';

var isCallable = require('is-callable');
var fnToStr = Function.prototype.toString;
var isNonArrowFnRegex = /^\s*function/;
var isArrowFnWithParensRegex = /^\([^)]*\) *=>/;
var isArrowFnWithoutParensRegex = /^[^=]*=>/;

/** @param {unknown} fn */
module.exports = function isArrowFunction(fn) {
if (!isCallable(fn)) {
return false;
}

/** @type {string} */
var fnStr = fnToStr.call(fn);
return fnStr.length > 0
&& !isNonArrowFnRegex.test(fnStr)
&& (isArrowFnWithParensRegex.test(fnStr) || isArrowFnWithoutParensRegex.test(fnStr));

var classRe = /^\s*class[\s/{]/;
var stripRe = /^\s+|\s+$/g;

var firstNonSpace = fnStr.search(/\S/);
var quote = fnStr.search(/['"`]/);

var paren = fnStr.indexOf('(');
var brace = fnStr.indexOf('{');
var arrow = fnStr.indexOf('=>');
var slash = fnStr.indexOf('/');

if (firstNonSpace === -1 || arrow === -1 || classRe.test(fnStr)) {
return false;
}
if (brace === -1 || paren === -1 || paren === firstNonSpace) {
return true;
}

var puncts = [
arrow, brace, paren, slash, quote
];
for (var i = 0; i < puncts.length; ++i) {
puncts[i] = puncts[i] === -1 ? Infinity : puncts[i];
}
puncts.sort(function (a, b) {
return a - b;
});
if (puncts[0] === quote) {
return false;
}
if (puncts[0] === arrow) {
return true;
}

var beforeParams = fnStr.slice(0, paren).replace(stripRe, '');
return !beforeParams || beforeParams === 'async';
};
3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,6 @@
"eslint": "=8.8.0",
"for-each": "^0.3.3",
"in-publish": "^2.0.1",
"make-arrow-function": "^1.2.0",
"make-async-function": "^1.0.0",
"make-generator-function": "^2.0.0",
"npmignore": "^0.3.0",
"nyc": "^10.3.2",
"safe-publish-latest": "^2.0.0",
Expand Down
279 changes: 279 additions & 0 deletions test/fixtures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
// @ts-check

'use strict';

/**
* @typedef {{
* source: string
* minNodeVersion: number
* }} SourceConfig
*/

/** @typedef {(string | SourceConfig)[]} Sources */

var getNodeVersion = function () {
if (typeof process === 'object' && process.versions && process.versions.node) {
var major = process.versions.node.match(/^\d+/);

if (!major) {
return -1;
}

return Number(major[0]);
}

return -1;
};

var MODERN_NODE_CUTOFF = 12;
var CURRENT_NODE_VERSION = getNodeVersion();

/**
* @param {string | SourceConfig} objOrSource
* @returns {any}
*/
var parseFromSource = function (objOrSource) {
var minNodeVersion = -1;
var source = objOrSource;

if (typeof objOrSource !== 'string') {
minNodeVersion = objOrSource.minNodeVersion;
source = objOrSource.source;
}

if (CURRENT_NODE_VERSION < minNodeVersion) {
return null;
}

try {
// eslint-disable-next-line no-new-func
return new Function('return (' + source + ')')();
} catch (e) {
if (CURRENT_NODE_VERSION >= MODERN_NODE_CUTOFF) {
// anything we pass to this function should be valid syntax as of modern node versions
if (e instanceof SyntaxError) {
throw new SyntaxError('Invalid source: ' + source);
}
throw e;
}
// if invalid syntax in older versions, we simply ignore them
return null;
}
};

/**
* @param {Sources} objsOrSources
*/
var parseAllFromSource = function (objsOrSources) {
var objs = [];
for (var i = 0; i < objsOrSources.length; ++i) {
var obj = parseFromSource(objsOrSources[i]);
if (obj) {
objs.push(obj);
}
}
return objs;
};

/** @type {Sources} */
var syncNonArrowFunctions = [
'function () {}',
'function foo() {}',
'function foo() { "=>"; }',
'function foo() { () => {} }',
'function (a = () => {}) {\n return a();\n }',
{
minNodeVersion: 10,
source: '({\n "() => {}"() {}\n })["() => {}"]'
},
'function/* => */() { }',
'function name/* => */() { }',
'function/* => */name() { }',
'function(/* => */) { }',
'function name(/* => */) { }',

'({ prop: function () {} }).prop',
'({ prop: function () { () => {} } }).prop',
'({ "=>": function () {} })["=>"]',
'({ "": function () {} })[""]'
];

/** @type {Sources} */
var syncArrowFunctions = [
'() => {}',
'(a, b) => a * b',
'() => 42',
'() => function () {}',
'() => x => x * x',
'y => x => x * x',
'x => x * x',
'x => { return x * x; }',
'(x, y) => { return x + x; }',
'(a = Math.random(10)) => {}',
'(a = function () {\n if (Math.random() < 0.5) { return 42; }\n return "something else";\n}) => a()',
'(a = function () {}) => a()',
'({\n "function name": () => { }\n })["function name"]',
'({\n function文: () => { }\n }).function文',
'/* function */x => { }',
'x/* function */ => { }',
'/* function */() => { }',
'()/* function */ => { }',
'(/* function */) => {}',

'({ prop: () => {} }).prop',
'({ prop: () => { function x() { } } }).prop',
'({ prop: x => { function x() { } } }).prop',
'({ function: () => {} }).function',
'({ "": () => {} })[""]'
];
/** @type {Sources} */
var asyncNonArrowFunctions = [
'async function () {}',
'async function foo() {}',
'async function () { "=>" }',
'async function foo() { "=>" }',

'({ prop: async function () {} }).prop'
];
/** @type {Sources} */
var asyncArrowFunctions = [
'async (a, b) => a * b',
'async x => {}',
'async () => {}',
'async () => { function f() {} }',

'({ prop: async () => {} }).prop'
];
/** @type {Sources} */
var syncGeneratorFunctions = [
'function* () { var x = yield; return x || 42; }',
'function* gen() { var x = yield; return x || 42; }',
'({ * concise() { var x = yield; return x || 42; } }).concise',
'({ prop: function* () {} }).prop'
];
/** @type {Sources} */
var asyncGeneratorFunctions = [
'async function* () {}',
'async function* () { yield "=>" }',
'({ prop: async function* () {} }).prop'
];
/** @type {Sources} */
var syncMethods = [
'({ method() {} }).method',
'({ method() {} }).method',
'({ method() { return "=>" } }).method',
'({ method() { () => {} } }).method',
{
minNodeVersion: 10,
source: '({ "=>"() {} })["=>"]'
},
{
minNodeVersion: 10,
source: '({ "x =>"() {} })["x =>"]'
},
{
minNodeVersion: 10,
source: '({ ""() {} })[""]'
},
{
minNodeVersion: 10,
source: '(() => { var obj1 = { "x => {}": "a" }; var obj2 = { [obj1["x => {}"]]() {} }; return obj2.a })()'
},
{
minNodeVersion: 10,
source: '(() => { var obj1 = { "x => {}": "a" }; var obj2 = { [obj1[\'x => {}\']]() {} }; return obj2.a })()'
},
{
minNodeVersion: 10,
source: '(() => { var obj1 = { "x => {}": "a" }; var obj2 = { [obj1[`x => {}`]]() {} }; return obj2.a })()'
}
];
/** @type {Sources} */
var asyncMethods = [
'({ async method() {} }).method',
'({ async method() {} }).method',
'({ async method() { return "=>" } }).method',
'({ async method() { () => {} } }).method',
{
minNodeVersion: 10,
source: '({ async "=>"() {} })["=>"]'
},
{
minNodeVersion: 10,
source: '({ async "x =>"() {} })["x =>"]'
},
{
minNodeVersion: 10,
source: '({ async ""() {} })[""]'
}
];
/** @type {Sources} */
var syncGeneratorMethods = [
'({ *method() {} }).method',
'({ *method() {} }).method',
'({ *method() { return "=>" } }).method',
'({ *method() { () => {} } }).method',
{
minNodeVersion: 10,
source: '({ *"=>"() {} })["=>"]'
},
{
minNodeVersion: 10,
source: '({ *"x =>"() {} })["x =>"]'
},
{
minNodeVersion: 10,
source: '({ *""() {} })[""]'
}
];
/** @type {Sources} */
var asyncGeneratorMethods = [
'({ async *method() {} }).method',
'({ async *method() {} }).method',
'({ async *method() { return "=>" } }).method',
'({ async *method() { () => {} } }).method',
{
minNodeVersion: 10,
source: '({ async *"=>"() {} })["=>"]'
},
{
minNodeVersion: 10,
source: '({ async *"x =>"() {} })["x =>"]'
},
{
minNodeVersion: 10,
source: '({ async *""() {} })[""]'
}
];
/** @type {Sources} */
var classes = [
'class {}',
'class X { }',
'class { "=>" }',
'class X { m() { return "=>" } }',
'class X { p = () => {} }'
];

var helpers = {
parseFromSource: parseFromSource,
parseAllFromSource: parseAllFromSource
};

var fixtures = {
syncNonArrowFunctions: parseAllFromSource(syncNonArrowFunctions),
syncArrowFunctions: parseAllFromSource(syncArrowFunctions),
asyncNonArrowFunctions: parseAllFromSource(asyncNonArrowFunctions),
asyncArrowFunctions: parseAllFromSource(asyncArrowFunctions),
syncGeneratorFunctions: parseAllFromSource(syncGeneratorFunctions),
asyncGeneratorFunctions: parseAllFromSource(asyncGeneratorFunctions),
syncMethods: parseAllFromSource(syncMethods),
asyncMethods: parseAllFromSource(asyncMethods),
syncGeneratorMethods: parseAllFromSource(syncGeneratorMethods),
asyncGeneratorMethods: parseAllFromSource(asyncGeneratorMethods),
classes: parseAllFromSource(classes)
};

module.exports = {
helpers: helpers,
fixtures: fixtures
};
Loading
Loading