How can I rewrite this while loop in a JSLint-approved way? - javascript

Looking at the the "Streams 2 & 3 (pull) example" from: https://github.com/jprichardson/node-fs-extra#walk
var items = [] // files, directories, symlinks, etc
var fs = require('fs-extra')
fs.walk(TEST_DIR)
.on('readable', function () {
var item
while ((item = this.read())) {
items.push(item.path)
}
})
.on('end', function () {
console.dir(items) // => [ ... array of files]
})
Latest version of JSLint complaints about the while:
Unexpected statement '=' in expression position.
while ((item = this.read())) {
Unexpected 'this'.
while ((item = this.read())) {
I'm trying to figure out how to write this in a JSLint-approved way. Any suggestions?
(Note: I'm aware there are other JSLint violations in this code ... I know how to fix those ...)

If you're really interested in writing this code like Douglas Crockford (the author of JSLint), you would use recursion instead of a while loop, since there are tail call optimizations in ES6.
var items = [];
var fs = require("fs-extra");
var files = fs.walk(TEST_DIR);
files.on("readable", function readPaths() {
var item = files.read();
if (item) {
items.push(item.path);
readPaths();
}
}).on("end", function () {
console.dir(items);
});

Related

Why does escodegen and esprima generate a parenthesis wrapper on my source code?

I am using escodegen to add an ending code on my statement as below. In the leave method, I append a .toArray() call on the end of the statement.
const esprima = require('esprima');
const estraverse = require('estraverse');
const escodegen = require('escodegen');
const ast = esprima.parse('db.find()');
let finished = false;
estraverse.traverse(ast, {
leave: (node, parent) => {
if (node.type === esprima.Syntax.ExpressionStatement && !finished) {
finished = true;
let statement = escodegen.generate(node);
statement = `${statement.substring(0, statement.lastIndexOf(';'))}.toArray()`;
const findAst = esprima.parse(statement);
node.arguments = findAst.body[0].expression.arguments;
node.callee = findAst.body[0].expression.callee;
node.type = findAst.body[0].expression.type;
}
},
});
const generated = escodegen.generate(ast);
console.log('generated code:', generated);
The output from above code is: generated code: (db.find().toArray()).
I don't understand why it wraps a parenthesis on my source code. Is there anything wrong in my source code?
You are generating an incorrect AST. An ExpressionStatement has the form {type: "ExpressionStatement", expression... } .
You are modifying your ExpressionStatement, attaching to it arguments and callee and you are changing its type (to CallExpression). Here:
node.arguments = findAst.body[0].expression.arguments;
node.callee = findAst.body[0].expression.callee;
node.type = findAst.body[0].expression.type;
Resulting a weird AST.
You can see it simply with : console.log('generated ast: %j', ast);
A quick solution is attach mentioned parts where them belong (to expression). Resulting:
let finished = false;
estraverse.traverse(ast, {
leave: (node, parent) => {
if (node.type === esprima.Syntax.ExpressionStatement && !finished) {
finished = true;
let statement = escodegen.generate(node);
statement = `${statement.substring(0, statement.lastIndexOf(';'))}.toArray()`;
console.log(statement);
const findAst = esprima.parse(statement);
node.expression.arguments = findAst.body[0].expression.arguments;
node.expression.callee = findAst.body[0].expression.callee;
node.expression.type = findAst.body[0].expression.type;
}
},
});
It will generate a correct AST, that will output the expected db.find().toArray();.
But I think the code is a bit complicated and does too much work, it parses db.find() then it generates code and parses it again.
Additionally you can return this.break() in leave to stop the traverse.
In my humble opinion it would be much clear:
var new_expr = {
type: "CallExpression",
callee: {
type: "MemberExpression",
computed: false,
object: null,
property: {
type: "Identifier",
name: "toArray"
}
},
arguments: []
};
const ast3 = esprima.parse('db.find()');
estraverse.traverse(ast3, {
leave: function(node, parent) {
if (node.type === esprima.Syntax.ExpressionStatement) {
new_expr.callee.object = node.expression;
node.expression = new_expr;
return this.break();
}
},
});
I hope you find this useful.

Error generating code with escodegen after node removal

First I created an esprima AST, then I want to remove a node using estraverse and finally regenerate the code with escodegen.
But I get an error.
The code I'm trying is:
var esprima = require('esprima');
var estraverse = require('estraverse');
var escodegen = require('escodegen');
(function () {
//build an ast with 2 lines of code
var ast = esprima.parse("console.log('1');\n console.log('2');")
console.log("original code:\n" + escodegen.generate(ast));
console.log();
//change one of the lines, works
ast = estraverse.replace(ast, {
enter: function (node) {
},
leave: function (node) {
if (node.type === esprima.Syntax.CallExpression) {
this.break();
return esprima.parse("console.log('patch');").body[0].expression;
}
}
});
console.log("patched code:\n" + escodegen.generate(ast));
console.log();
//remove one of the lines, error
ast = estraverse.replace(ast, {
enter: function (node) {
},
leave: function (node) {
if (node.type === esprima.Syntax.CallExpression) {
this.break();
return this.remove();
}
}
});
console.log("removed node:\n" + escodegen.generate(ast));
})()
The error trace is:
C:\temp\node_modules\escodegen\escodegen.js:2450
type = expr.type || Syntax.Property;
^
TypeError: Cannot read property 'type' of null
at CodeGenerator.generateExpression (C:\temp\node_modules\escodegen\escodegen.js:2450:20)
at CodeGenerator.ExpressionStatement (C:\temp\node_modules\escodegen\escodegen.js:1335:28)
at CodeGenerator.generateStatement (C:\temp\node_modules\escodegen\escodegen.js:2469:33)
at CodeGenerator.Program (C:\temp\node_modules\escodegen\escodegen.js:1717:43)
at CodeGenerator.generateStatement (C:\temp\node_modules\escodegen\escodegen.js:2469:33)
at generateInternal (C:\temp\node_modules\escodegen\escodegen.js:2490:28)
at Object.generate (C:\temp\node_modules\escodegen\escodegen.js:2558:18)
at C:\temp\bug1.js:35:45
at Object.<anonymous> (C:\temp\bug1.js:38:3)
at Module._compile (module.js:570:32)
Am I doing something wrong? Is this an error in escodegen or maybe in estraverse?
Thanks in advance.
I put an issue on github and I got an answer, I was making an invalid AST.
Deleting the CallExpression was leaving his parent ExpressionStatement empty and therefore invalid. The solution is simply deleting the ExpressionStatement.
This code works as expected:
var esprima = require('esprima');
var estraverse = require('estraverse');
var escodegen = require('escodegen');
(function () {
//build an ast with 2 lines of code
var ast = esprima.parse("console.log('1');\n console.log('2');")
console.log("original code:\n" + escodegen.generate(ast));
console.log();
//remove one of the lines, works!
var done = false;
ast = estraverse.replace(ast, {
enter: function (node) {
if (done)
return this.break();
if (node.type === esprima.Syntax.ExpressionStatement) {
done = true;
this.remove();
}
},
leave: function (node) {
if (done)
return this.break();
}
});
console.log("removed node:\n" + escodegen.generate(ast));
})()
The output:
original code:
console.log('1');
console.log('2');
removed node:
console.log('2');
It appears as though one reason this can occur is when code is removed that leaves an empty arrow function body. For example, the original code:
() => console.log(1);
Resulting in:
() =>
With one solution being:
() => { console.log(1); }
Arguably the parent should be removed too in this instance, just might be a little tricky if it was in practice something like
useEffect(() => console.log(1))

RegExp working in JSFiddle but not in nodejs

I have the regular expression:
/(?:!!)(.+)(?:!!)(?:\(zoomOn )([\S,]+)(?:\))/g
It matches something like !!some text!!(zoomOn 1,2,3).
This works okay in the browser (JSFiddle here) but not in Node. I am writing in ES2015, using Babel and the es2015 preset.
For extra insight this is for a Showdown extension. I noticed the twitter extension add some extra \ to the RegExps. Is this a quirk of Node/ES5 I'm not aware of?
Update
I was hoping I wouldn't need to post the code for Node since I thought it would just be a node quirk.
Anyway, the code is for an extension to Showdown:
# extensions.js
export const manipulationAPIExtensions = () => [
{
// [zoomOn node1,node2,node3,...](some text)
type: 'lang',
filter: (text, converter, options) => {
const toReturn = text.replace(/(?:!!)(.+)(?:!!)(?:\(zoomOn )([\S,]+)(?:\))/g, (match, innerText, nodeString) => {
const nodes = nodeString.split(/\s*,\s*/);
let nodeArrayAsString = '[';
nodes.forEach(node => {
nodeArrayAsString += `'${node}',`;
});
nodeArrayAsString += ']';
return `<a onclick="pathwayInstance.manipulator.zoomOn(${nodeArrayAsString})">${text}</a>`;
});
return toReturn;
},
},
];
This is used in Showdown as follows:
export const getShowdown = (KaavioInstance) => {
window.diagram = KaavioInstance;
Showdown.extension('kaavio', manipulationAPIExtensions());
return new Showdown.Converter({
extensions: ['kaavio'],
});
};
And then in my unit test:
describe('CustomMarkdown', () => {
// Don't really need Kaavio since we are only checking the output HTML
const mockKaavioInstance = {};
const converter = getShowdown(mockKaavioInstance);
console.log(converter.getAllExtensions())
describe('Kaavio', () => {
it('should return the correct HTML from the markdown', () => {
const markdown = normalize(fs.readFileSync(`${__dirname}/extensions/Kaavio.md`, 'utf8'));
const HTML = normalize(fs.readFileSync(`${__dirname}/extensions/Kaavio.html`, 'utf8'));
const output = converter.makeHtml(markdown);
assert.equal(output, HTML);
});
});
});
The unit test fails since no match is found.
If I do something simple like the below it works. Of course the unit test doesn't work but if I console.log it out then I get the expected result of matched.
# extensions.js
export const manipulationAPIExtensions = () => [
{
// [zoomOn node1,node2,node3,...](some text)
type: 'lang',
filter: (text, converter, options) => {
const toReturn = text.replace(/./g, (match) => {
return 'matched';
});
return toReturn;
},
},
];
That works for me in Node, just tried it, but that fiddle uses an alert(). You need to use something like console.log() that Node understands.
var text = '!!some text!!(zoomOn node1)';
text.replace(/(?:!!)(.+)(?:!!)(?:\(zoomOn )([\S,]+)(?:\))/g, function (match, innerText, nodeString) {
console.log("matched!");
})
Maybe you have some other code you didn't post which is interfering?
This is embarrassing but a lesson in checking all called functions learned. The normalize() function that is called in these lines:
const markdown = normalize(fs.readFileSync(`${__dirname}/extensions/Kaavio.md`, 'utf8'));
const HTML = normalize(fs.readFileSync(`${__dirname}/extensions/Kaavio.html`, 'utf8'));
was replacing white spaces with bullet characters... Thanks for the help regardless!

Javascript class returning different values in console.log

I have the following classes (unnecessary details cut out here to make it more readable):
class CollectionManager {
constructor(){
this.collectionList = {};
}
initialize(collections){
...
}
populate(){
var collectionObjs = Object.keys(this.collectionList).map(function(key){
return collectionManager.collectionList[key];
});
return Promise.all(collectionObjs.map(function(collection){
collection.populateVideos();
}));
}
}
.
class Collection {
constructor(data){
this.collectionInfo = data;
this.videoArray = [];
}
populateVideos(){
var collectionKey = this.collectionInfo.COLLECTIONID;
var vChannels = Object.keys(this.collectionInfo.channels);
return Promise.all(vChannels.map(requestVideos))
.then(function (results) {
var videoIdArray = [];
return videoIdArray = [].concat.apply([], results);
}).then(function(arrVideoIds){
var groups = [];
for (var i = 0; i < arrVideoIds.length; i += 50) {
groups.push(arrVideoIds.slice(i, i + 50));
}
return groups;
}).then(function(chunkedArrVideoIds){
return Promise.all(chunkedArrVideoIds.map(requestVideoData)).then(function (results) {
var videoTileArray = [].concat.apply([], results);
collectionManager.collectionList[collectionKey].videoArray = videoTileArray;
return videoTileArray;
});
});
}
displayCollection(){
console.log(this.collectionInfo.COLLECTIONID);
console.log(collectionManager.collectionList);
console.log(collectionManager.collectionList[1]);
console.log(collectionManager.collectionList[1].videoArray);
And I call these classes like I would any normal promise.
collectionManager.populate().then(
function(){
collectionManager.displayCollections()
}
);
Now my problem is that when I call this code and read what is on the console, the videoArray is completely empty in the 4th console log. collectionManager.collectionList[1] contains a full object that has a videoArray with a length of 100 with all of my videos properly inside of it. But if I call collectionManager.collectionList[1].videoArray it is empty like it hasn't been filled. As far as I'm aware those should be calling the same exact place but it is giving different results.
Does anyone see where I messed up?
In the populate function, your Promise.all ... map is returning an array of undefined, which would be immediately resolved by Promise.all
You should do as follows
populate(){
var collectionObjs = Object.keys(this.collectionList).map(function(key){
return collectionManager.collectionList[key];
});
return Promise.all(collectionObjs.map(function(collection){
return collection.populateVideos();
}));
}
but, as you are using Class - you're already using more modern javascript
so
populate(){
var collectionObjs = Object.keys(this.collectionList).map(key => collectionManager.collectionList[key]);
return Promise.all(collectionObjs.map(collection => collection.populateVideos()));
}
Would be quite acceptable
as an aside, your class Collection can also be made cleaner (in my opinion) using arrow functions, and better promise chaining
class Collection {
constructor(data) {
this.collectionInfo = data;
this.videoArray = [];
}
populateVideos() {
var collectionKey = this.collectionInfo.COLLECTIONID;
var vChannels = Object.keys(this.collectionInfo.channels);
return Promise.all(vChannels.map(requestVideos))
.then(results => [].concat.apply([], results))
.then(arrVideoIds => {
var groups = [];
for (var i = 0; i < arrVideoIds.length; i += 50) {
groups.push(arrVideoIds.slice(i, i + 50));
}
return groups;
)
.then(chunkedArrVideoIds => Promise.all(chunkedArrVideoIds.map(requestVideoData)))
.then(function(results) {
var videoTileArray = [].concat.apply([], results);
collectionManager.collectionList[collectionKey].videoArray = videoTileArray;
return videoTileArray;
});
}
displayCollection() {
console.log(this.collectionInfo.COLLECTIONID);
console.log(collectionManager.collectionList);
console.log(collectionManager.collectionList[1]);
console.log(collectionManager.collectionList[1].videoArray);
}
}
I would avoid console.log() debugging, unless you really want to frustrate yourself. This could be a simple matter of console.log() not behaving as you expect. Instead, I would suggest puttting a debugger statement in displayCollection(). All you have to do is literally add the line debugger; into the code of that function and have chrome dev tools open when you run it. Execution will halt on that line and allow you to inspect application state with chrome dev tools(or the dev tools of whatever browser you're using). Based on the four print statements you have there, I think it might just be that it's not printing as you expect.

Uglify JS - compressing unused variables

Uglify has a "compression" option that can remove unused variables...
However, if I stored some functions in an object like this....
helpers = {
doSomething: function () { ... },
doSomethingElese: function () { ... }
}
... is there a way to remove helpers.doSomething() if it's never accessed?
Guess I want to give the compressor permission to change my object.
Any ideas if it's possible? Or any other tools that can help?
Using a static analyzer like Uglify2 or Esprima to accomplish this task is somewhat nontrivial, because there are lots of situations that will call a function that are difficult to determine. To show the complexity, there's this website:
http://sevinf.github.io/blog/2012/09/29/esprima-tutorial/
Which attempts to at least identify unused functions. However the code as provided on that website will not work against your example because it is looking for FunctionDeclarations and not FunctionExpressions. It is also looking for CallExpression's as Identifiers while ignoring CallExpression's that are MemberExpression's as your example uses. There's also a problem of scope there, it doesn't take into account functions in different scopes with the same name - perfectly legal Javascript, but you lose fidelity using that code as it'll miss some unused functions thinking they were called when they were not.
To handle the scope problem, you might be able to employ ESTR (https://github.com/clausreinke/estr), to help figure out the scope of the variables and from there the unused functions. Then you'll need to use something like escodegen to remove the unused functions.
As a starting point for you I've adapted the code on that website to work for your very specific situation provided, but be forwarned, it will have scope issue.
This is written for Node.js, so you'll need to get esprima with npm to use the example as provided, and of course execute it with node.
var fs = require('fs');
var esprima = require('esprima');
if (process.argv.length < 3) {
console.log('Usage: node ' + process.argv[1] + ' <filename>');
process.exit(1);
}
notifydeadcode = function(data){
function traverse(node, func) {
func(node);
for (var key in node) {
if (node.hasOwnProperty(key)) {
var child = node[key];
if (typeof child === 'object' && child !== null) {
if (Array.isArray(child)) {
child.forEach(function(node) {
traverse(node, func);
});
} else {
traverse(child, func);
}
}
}
}
}
function analyzeCode(code) {
var ast = esprima.parse(code);
var functionsStats = {};
var addStatsEntry = function(funcName) {
if (!functionsStats[funcName]) {
functionsStats[funcName] = {calls: 0, declarations:0};
}
};
var pnode = null;
traverse(ast, function(node) {
if (node.type === 'FunctionExpression') {
if(pnode.type == 'Identifier'){
var expr = pnode.name;
addStatsEntry(expr);
functionsStats[expr].declarations++;
}
} else if (node.type === 'FunctionDeclaration') {
addStatsEntry(node.id.name);
functionsStats[node.id.name].declarations++;
} else if (node.type === 'CallExpression' && node.callee.type === 'Identifier') {
addStatsEntry(node.callee.name);
functionsStats[node.callee.name].calls++;
}else if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression'){
var lexpr = node.callee.property.name;
addStatsEntry(lexpr);
functionsStats[lexpr].calls++;
}
pnode = node;
});
processResults(functionsStats);
}
function processResults(results) {
//console.log(JSON.stringify(results));
for (var name in results) {
if (results.hasOwnProperty(name)) {
var stats = results[name];
if (stats.declarations === 0) {
console.log('Function', name, 'undeclared');
} else if (stats.declarations > 1) {
console.log('Function', name, 'decalred multiple times');
} else if (stats.calls === 0) {
console.log('Function', name, 'declared but not called');
}
}
}
}
analyzeCode(data);
}
// Read the file and print its contents.
var filename = process.argv[2];
fs.readFile(filename, 'utf8', function(err, data) {
if (err) throw err;
console.log('OK: ' + filename);
notifydeadcode(data);
});
So if you plop that in a file like deadfunc.js and then call it like so:
node deadfunc.js test.js
where test.js contains:
helpers = {
doSomething:function(){ },
doSomethingElse:function(){ }
};
helpers.doSomethingElse();
You will get the output:
OK: test.js
Function doSomething declared but not called
One last thing to note: attempting to find unused variables and functions might be a rabbit hole because you have situations like eval and Functions created from strings. You also have to think about apply and call etc, etc. Which is why, I assume, we don't have this capability in the static analyzers today.

Categories

Resources