Skip to content
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

Check for mozIndexedDB; close #15 #44

Merged
merged 2 commits into from
Oct 6, 2015
Merged
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"dependencies": {
"chalk": "1.1.1",
"es6-promisify": "3.0.0",
"eslint": "1.6.0",
"yargs": "3.26.0",
"yauzl": "2.3.1"
}
Expand Down
10 changes: 10 additions & 0 deletions src/const.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
export const DEFLATE_COMPRESSION = 8;
export const NO_COMPRESSION = 0;

export const ESLINT_ERROR = 2;
export const ESLINT_NOTICE = 0;
export const ESLINT_WARNING = 1;

export const VALIDATION_ERROR = 'error';
export const VALIDATION_NOTICE = 'notice';
export const VALIDATION_WARNING = 'warning';

export const ESLINT_TYPES = {
0: VALIDATION_NOTICE,
1: VALIDATION_WARNING,
2: VALIDATION_ERROR,
};

export const MESSAGE_TYPES = [
VALIDATION_ERROR,
VALIDATION_NOTICE,
Expand Down
49 changes: 49 additions & 0 deletions src/javascript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import path from 'path';

import ESLint from 'eslint';

import { ESLINT_TYPES } from 'const';
import * as messages from 'messages';
import ESLintRules from 'rules';


export default class JavaScriptScanner {

constructor(code, filename) {
this.code = code;
this.filename = filename;
}

scan() {
return new Promise((resolve) => {
// ESLint is synchronous and doesn't accept streams, so we need to
// pass it the entire source file as a string.
let eslint = new ESLint.CLIEngine({
ignore: false,
rulePaths: [path.join('src', 'rules')],
rules: ESLintRules,
useEslintrc: false,
});

var validatorMessages = [];
var report = eslint.executeOnText(this.code, this.filename);

for (let message of report.results[0].messages) {
validatorMessages.push({
id: message.ruleId.toUpperCase(),
message: messages[message.ruleId.toUpperCase()].message,
description: messages[message.ruleId.toUpperCase()].description,
code: message.source,
file: this.filename,
line: message.line,
column: message.column,
severity: ESLINT_TYPES[message.severity],
});
}

resolve(validatorMessages);
});
}


}
1 change: 1 addition & 0 deletions src/messages/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Here we can re-export all our message so one can just do
// import * as messages from 'messages';

export * from './javascript';
export * from './layout';
14 changes: 14 additions & 0 deletions src/messages/javascript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { gettext as _ } from 'utils';


export const MOZINDEXEDDB = {
code: 'MOZINDEXEDDB',
message: _('mozIndexedDB has been removed; use indexedDB instead.'),
description: _('mozIndexedDB has been removed; use indexedDB instead.'),
};

export const MOZINDEXEDDB_PROPERTY = {
code: 'MOZINDEXEDDB_PROPERTY',
message: _('mozIndexedDB used as an object key/property.'),
description: _('mozIndexedDB has been removed; use indexedDB instead.'),
};
6 changes: 6 additions & 0 deletions src/rules/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ESLINT_ERROR, ESLINT_WARNING } from 'const';

export default {
mozIndexedDB: ESLINT_ERROR,
mozIndexedDB_property: ESLINT_WARNING,
};
11 changes: 11 additions & 0 deletions src/rules/mozIndexedDB.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = function(context) {
return {
Identifier: function(node) {
// Catches `var foo = mozIndexedDB;`.
if (node.name === 'mozIndexedDB' &&
node.parent.type !== 'MemberExpression') {
return context.report(node, 'MOZINDEXEDDB');
}
},
};
};
18 changes: 18 additions & 0 deletions src/rules/mozIndexedDB_property.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = function(context) {
return {
Identifier: function(node) {
// Catches `var foo = 'mozIndexedDB'; var myDatabase = window[foo];`.
if (node.parent.init && node.parent.init.value === 'mozIndexedDB') {
return context.report(node, 'MOZINDEXEDDB_PROPERTY');
}
},
MemberExpression: function(node) {
// Catches `var foo = window.mozIndexedDB;` and
// `var foo = window['mozIndexedDB'];`.
if (node.property.name === 'mozIndexedDB' ||
node.property.value === 'mozIndexedDB') {
context.report(node, 'MOZINDEXEDDB_PROPERTY');
}
},
};
};
10 changes: 10 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
/*
* Implementation of String.endsWith, which errors in node.
*
*/
export function endsWith(string, suffix) {
string = String(string);
suffix = String(suffix);
return string.indexOf(suffix, string.length - suffix.length) !== -1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's a test for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Damn, good catch. I actually was thinking "I think I left out a test for something" but couldn't remember what.

}

/*
* Template tag for removing whitespace and new lines
* in order to be able to use multiline template strings
Expand Down
90 changes: 69 additions & 21 deletions src/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import * as messages from 'messages';
import * as exceptions from 'exceptions';
import * as constants from 'const';

import Xpi from 'xpi';
import JavaScriptScanner from 'javascript';
import Collector from 'collector';
import Xpi from 'xpi';

export var lstat = promisify(fs.lstat);

Expand All @@ -24,6 +25,12 @@ export default class Validator {
this.collector = new Collector();
}

addToCollector(message) {
var messageType = message.severity.charAt(0).toUpperCase() +
message.severity.slice(1);
this.collector[`add${messageType}`](message);
}

handleError(err) {
if (this.config.stack === true) {
console.error(err.stack);
Expand Down Expand Up @@ -75,6 +82,67 @@ export default class Validator {
return output;
}

scan(_Xpi=Xpi) {
return new Promise((resolve, reject) => {
this.checkFileExists(this.packagePath)
.then(() => {
this.xpi = new _Xpi(this.packagePath);
return this.xpi.getJSFiles();
})
.then((jsFiles) => {
return this.scanJSFiles(jsFiles);
})
.then(() => {
this.print();
resolve();
})
.catch((err) => {
if (err instanceof exceptions.DuplicateZipEntryError) {
this.collector.addError(messages.DUPLICATE_XPI_ENTRY);
this.print();
} else {
this.handleError(err);
}

reject(err);
});
});
}

scanJSFiles(jsFiles) {
return new Promise((resolve, reject) => {
// Resolve once every file in the XPI has been checked.
var jsFilesPromises = [];

for (let filename of jsFiles) {
jsFilesPromises.push(this.scanJSFile(filename));
}

return Promise.all(jsFilesPromises)
.then(() => {
resolve(this.collector);
}).catch(reject);
});
}

scanJSFile(filename) {
return new Promise((resolve, reject) => {
this.xpi.getFileAsString(filename)
.then((code) => {
let jsScanner = new JavaScriptScanner(code, filename);
return jsScanner.scan();
})
.then((validatorMessages) => {
for (let message of validatorMessages) {
this.addToCollector(message);
}

resolve();
})
.catch(reject);
});
}

toJSON(pretty=false) {
var args = [this.output];
if (pretty === true) {
Expand All @@ -83,24 +151,4 @@ export default class Validator {
}
return JSON.stringify.apply(null, args);
}

scan(_Xpi=Xpi) {
return this.checkFileExists(this.packagePath)
.then(() => {
this.xpi = new _Xpi(this.packagePath);
return this.xpi.getMetaData();
})
.then((metadata) => {
// Do something useful with package here.
console.log(metadata);
})
.catch((err) => {
if (err instanceof exceptions.DuplicateZipEntryError) {
this.collector.addError(messages.DUPLICATE_XPI_ENTRY);
this.print();
} else {
return this.handleError(err);
}
});
}
}
42 changes: 40 additions & 2 deletions src/xpi.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import yauzl from 'yauzl';

import { DuplicateZipEntryError } from 'exceptions';

import { endsWith } from 'utils';

/*
* Simple Promise wrapper for the Yauzl unzipping lib to unpack add-on .xpis.
Expand Down Expand Up @@ -96,7 +96,45 @@ export default class Xpi {
resolve(readStream);
});
})
.catch((err) => reject(err));
.catch(reject);
});
}

getFileAsString(path) {
return new Promise((resolve, reject) => {
return this.getFileAsStream(path)
.then((fileStream) => {
var fileString = '';
fileStream.on('data', (chunk) => {
fileString += chunk;
});

// Once the file is assembled, resolve the promise.
fileStream.on('end', () => {
resolve(fileString);
});
})
.catch(reject);
});
}

getJSFiles() {
return new Promise((resolve, reject) => {
return this.getMetaData()
.then((metadata) => {
let jsFiles = [];

for (let filename in metadata) {
// TODO: Check for JS files better than this (follow require path)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question - I wonder how the current validator deals with the possibility of imports including requires/imports of files that don't end in .js?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's likely something for a separate issue and could be its own can of worms.

A brute-force way would be to get ESLint to try and parse every file, and anything that throws a parsing error is assumed to not be JS. There are edge cases for that implementation as well, but it would prevent people sneaking code past the validator by naming every file .javascript or something.

// of add-on?
if (endsWith(filename, '.js')) {
jsFiles.push(filename);
}
}

resolve(jsFiles);
})
.catch(reject);
});
}
}
Binary file added tests/example.xpi
Binary file not shown.
Loading