Update eslint-config-amo, integrate Prettier and addons-frontend inspired configuration (#2144)
* chore(package): update eslint-config-amo to version 1.8.0 * chore(package): update lockfile https://npm.im/greenkeeper-lockfile * Update to eslint-config-amo 1.8.3 * Integrate prettier and addons-frontend inspired configs * Run prettier * Fix some more docs inconsistencies * Some more eslint/prettier config cleanups * Reformat bin/ * Fix dependency updates * Fix readme * Keep es5 trailing comma support Fixes #2084
This commit is contained in:
Родитель
3bc138152d
Коммит
225c9130df
|
@ -2,17 +2,19 @@
|
|||
"rules":{
|
||||
"no-console": 0,
|
||||
// Inherited from airbnb via eslint-config-amo
|
||||
"function-paren-newline": 0
|
||||
"function-paren-newline": 0,
|
||||
"prettier/prettier": 2
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
},
|
||||
"extends": "amo",
|
||||
"extends": ["amo", prettier],
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"async-await"
|
||||
"async-await",
|
||||
"prettier"
|
||||
],
|
||||
"settings": {
|
||||
"async-await/space-after-async": 2,
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# exclude everything by default
|
||||
*.*
|
||||
|
||||
# exclude these files
|
||||
bin/download-import-tag
|
||||
bin/list-firefox-tags
|
||||
package-lock.json
|
||||
LICENSE
|
||||
|
||||
# exclude these directories
|
||||
/tmp/
|
||||
/dist/
|
||||
/locale/
|
||||
/node_modules/
|
||||
/tests/fixtures/
|
||||
|
||||
# white-list files we want to process
|
||||
!*.js
|
||||
!*.md
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"arrowParens": "always",
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"proseWrap": "never",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "bin/*",
|
||||
"options": {
|
||||
"parser": "babylon"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
Code of conduct
|
||||
===============
|
||||
# Code of conduct
|
||||
|
||||
This repository is governed by Mozilla's code of conduct and etiquette guidelines. For more details please see the [Mozilla Community Participation Guidelines] (https://www.mozilla.org/about/governance/policies/participation/) and [Developer Etiquette Guidelines] (https://bugzilla.mozilla.org/page.cgi?id=etiquette.html).
|
||||
This repository is governed by Mozilla's code of conduct and etiquette guidelines. For more details please see the [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/) and [Developer Etiquette Guidelines](https://bugzilla.mozilla.org/page.cgi?id=etiquette.html).
|
||||
|
|
|
@ -10,7 +10,6 @@ Here are links to all the sections in this document:
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
|
||||
|
||||
- [Picking an issue](#picking-an-issue)
|
||||
- [Installation](#installation)
|
||||
- [Testing the linter](#testing-the-linter)
|
||||
|
@ -24,17 +23,13 @@ Here are links to all the sections in this document:
|
|||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
|
||||
# Picking an issue
|
||||
|
||||
For first-time contributors or those who want to start with a small task: [check out our list of good first bugs](https://github.com/mozilla/addons-linter/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+bug%22). These issues have an assigned mentor to help you out and are great issues for learning about the linter and our development process.
|
||||
|
||||
If you're already familiar with the project or would like take on something a little more challenging, please take a look at the [contrib: welcome](https://github.com/mozilla/addons-linter/issues?q=is%3Aissue+is%3Aopen+label%3A"contrib%3A+welcome) issues.
|
||||
|
||||
If you'd like to work on a bug, please comment on it to let the maintainers know.
|
||||
If someone else has already commented and taken up that bug, please refrain from working on it and submitting
|
||||
a PR without asking the maintainers as it leads to unnecessary duplication of effort.
|
||||
|
||||
If you'd like to work on a bug, please comment on it to let the maintainers know. If someone else has already commented and taken up that bug, please refrain from working on it and submitting a PR without asking the maintainers as it leads to unnecessary duplication of effort.
|
||||
|
||||
# Installation
|
||||
|
||||
|
@ -52,59 +47,43 @@ To run the entire suite of tests once and exit, type:
|
|||
|
||||
npm run test-once
|
||||
|
||||
This is the same as the `npm test` command but it won't re-run automatically as
|
||||
you edit files.
|
||||
This is the same as the `npm test` command but it won't re-run automatically as you edit files.
|
||||
|
||||
## Run a single test
|
||||
|
||||
Instead of running the entire suite, you can run a single test by invoking
|
||||
the `jest` executable directly with the `-t` option to filter by test
|
||||
description. For example, if the test you'd like to run is defined in
|
||||
`tests/test.linter.js` and is described as
|
||||
"should collect an error when not an xpi/zip" then you could run it like this:
|
||||
Instead of running the entire suite, you can run a single test by invoking the `jest` executable directly with the `-t` option to filter by test description. For example, if the test you'd like to run is defined in `tests/test.linter.js` and is described as "should collect an error when not an xpi/zip" then you could run it like this:
|
||||
|
||||
./node_modules/.bin/jest -r tests/test.linter.js -f "not an xpi/zip"
|
||||
|
||||
## Debug a test
|
||||
|
||||
You can enter the [Node debugger](https://nodejs.org/api/debugger.html) by
|
||||
directly invoking the `npm run debug` command. For example,
|
||||
if the test you want to debug is defined in `tests/test.linter.js` then you
|
||||
could enter the debugger like this:
|
||||
You can enter the [Node debugger](https://nodejs.org/api/debugger.html) by directly invoking the `npm run debug` command. For example, if the test you want to debug is defined in `tests/test.linter.js` then you could enter the debugger like this:
|
||||
|
||||
node --inspect --inspect-brk ./node_modules/.bin/jest tests/test.linter.js -t 'flag potentially minified'
|
||||
|
||||
You could also put the `debugger` statement somewhere in the code to set a
|
||||
breakpoint.
|
||||
You could also put the `debugger` statement somewhere in the code to set a breakpoint.
|
||||
|
||||
# Build addons-linter
|
||||
|
||||
Type `npm run build` to build a new version of the libraries used by the
|
||||
`./bin/addons-linter` command. When successful, you will see newly built files in
|
||||
the `./dist/` directory.
|
||||
Type `npm run build` to build a new version of the libraries used by the `./bin/addons-linter` command. When successful, you will see newly built files in the `./dist/` directory.
|
||||
|
||||
# Creating a pull request
|
||||
|
||||
When you create a
|
||||
[pull request](https://help.github.com/articles/creating-a-pull-request/)
|
||||
for a new fix or feature, be sure to mention the issue
|
||||
number for what you're working on.
|
||||
The best way to do it is to mention the issue like
|
||||
this at the top of your description:
|
||||
When you create a [pull request](https://help.github.com/articles/creating-a-pull-request/) for a new fix or feature, be sure to mention the issue number for what you're working on. The best way to do it is to mention the issue like this at the top of your description:
|
||||
|
||||
Fixes #123
|
||||
|
||||
The issue number in this case is "123."
|
||||
The word *Fixes* is magical; GitHub will automatically close the issue when your
|
||||
pull request is merged.
|
||||
The issue number in this case is "123." The word _Fixes_ is magical; GitHub will automatically close the issue when your pull request is merged.
|
||||
|
||||
Please run [Prettier](https://github.com/mozilla/addons-linter/blob/master/README.md#Prettier) to format your code.
|
||||
|
||||
# Writing commit messages
|
||||
|
||||
Good commit messages serve at least three important purposes:
|
||||
|
||||
* They speed up the reviewing process.
|
||||
* They help us write good release notes.
|
||||
* They help future maintainers understand your change and the reasons behind it.
|
||||
- They speed up the reviewing process.
|
||||
- They help us write good release notes.
|
||||
- They help future maintainers understand your change and the reasons behind it.
|
||||
|
||||
Structure your commit message like this:
|
||||
|
||||
|
@ -128,11 +107,10 @@ From: [[http://git-scm.com/book/ch5-2.html]]
|
|||
> single space, with blank lines in between, but conventions vary here
|
||||
> ```
|
||||
|
||||
* Write the summary line and description of what you have done in the imperative mode, that is as if you were commanding someone. Start the line with "Fix", "Add", "Change" instead of "Fixed", "Added", "Changed".
|
||||
* Always leave the second line blank.
|
||||
* Be as descriptive as possible in the description. It helps reasoning about the intention of commits and gives more context about why changes happened.
|
||||
- Write the summary line and description of what you have done in the imperative mode, that is as if you were commanding someone. Start the line with "Fix", "Add", "Change" instead of "Fixed", "Added", "Changed".
|
||||
- Always leave the second line blank.
|
||||
- Be as descriptive as possible in the description. It helps reasoning about the intention of commits and gives more context about why changes happened.
|
||||
|
||||
Tips
|
||||
----
|
||||
## Tips
|
||||
|
||||
* If it seems difficult to summarize what your commit does, it may be because it includes several logical changes or bug fixes, and are better split up into several commits using `git add -p`.
|
||||
- If it seems difficult to summarize what your commit does, it may be because it includes several logical changes or bug fixes, and are better split up into several commits using `git add -p`.
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
### Describe the problem and steps to reproduce it:
|
||||
|
||||
(Please include as many details as possible.)
|
||||
|
||||
### What happened?
|
||||
|
@ -6,5 +7,5 @@
|
|||
### What did you expect to happen?
|
||||
|
||||
### Anything else we should know?
|
||||
(Please include screenshots and any relevant files.)
|
||||
|
||||
(Please include screenshots and any relevant files.)
|
||||
|
|
|
@ -2,13 +2,10 @@ Thanks for opening a Pull Request (PR), here's a few guidelines as to what we ne
|
|||
|
||||
Please delete anything that isn't relevant to your patch.
|
||||
|
||||
* [ ] This PR relates to an existing open issue and there are no existing
|
||||
PRs open for the same issue.
|
||||
* [ ] Add `Fixes #ISSUENUM` at the top of your PR.
|
||||
* [ ] Add a description of the the changes introduced in this PR.
|
||||
* [ ] The change has been successfully run locally.
|
||||
* [ ] Add tests to cover the changes added in this PR.
|
||||
- [ ] This PR relates to an existing open issue and there are no existing PRs open for the same issue.
|
||||
- [ ] Add `Fixes #ISSUENUM` at the top of your PR.
|
||||
- [ ] Add a description of the the changes introduced in this PR.
|
||||
- [ ] The change has been successfully run locally.
|
||||
- [ ] Add tests to cover the changes added in this PR.
|
||||
|
||||
Once you have met the above requirements please replace this section with
|
||||
a `Fixes #ISSUENUM` linking to the issue fixed by this PR along with an
|
||||
explanation of the changes. Thanks for your contribution!
|
||||
Once you have met the above requirements please replace this section with a `Fixes #ISSUENUM` linking to the issue fixed by this PR along with an explanation of the changes. Thanks for your contribution!
|
||||
|
|
95
README.md
95
README.md
|
@ -1,8 +1,4 @@
|
|||
[![Build Status](https://travis-ci.org/mozilla/addons-linter.svg?branch=master)](https://travis-ci.org/mozilla/addons-linter)
|
||||
[![Coverage Status](https://coveralls.io/repos/mozilla/addons-linter/badge.svg?branch=master&service=github)](https://coveralls.io/github/mozilla/addons-linter?branch=master)
|
||||
[![Dependency Status](https://david-dm.org/mozilla/addons-linter.svg)](https://david-dm.org/mozilla/addons-linter)
|
||||
[![devDependency Status](https://david-dm.org/mozilla/addons-linter/dev-status.svg)](https://david-dm.org/mozilla/addons-linter#info=devDependencies)
|
||||
[![npm version](https://badge.fury.io/js/addons-linter.svg)](https://badge.fury.io/js/addons-linter)
|
||||
[![Build Status](https://travis-ci.org/mozilla/addons-linter.svg?branch=master)](https://travis-ci.org/mozilla/addons-linter) [![Coverage Status](https://coveralls.io/repos/mozilla/addons-linter/badge.svg?branch=master&service=github)](https://coveralls.io/github/mozilla/addons-linter?branch=master) [![Dependency Status](https://david-dm.org/mozilla/addons-linter.svg)](https://david-dm.org/mozilla/addons-linter) [![devDependency Status](https://david-dm.org/mozilla/addons-linter/dev-status.svg)](https://david-dm.org/mozilla/addons-linter#info=devDependencies) [![npm version](https://badge.fury.io/js/addons-linter.svg)](https://badge.fury.io/js/addons-linter)
|
||||
|
||||
# addons-linter
|
||||
|
||||
|
@ -18,8 +14,7 @@ You can find more information about the linter and it's implemented rules in our
|
|||
|
||||
You need node.js to use the add-ons linter.
|
||||
|
||||
To validate your add-on locally, install the linter from
|
||||
[npm](http://nodejs.org/):
|
||||
To validate your add-on locally, install the linter from [npm](http://nodejs.org/):
|
||||
|
||||
```sh
|
||||
# Install globally so you can use the linter from any directory on
|
||||
|
@ -39,9 +34,7 @@ Alternatively you can point it at a directory:
|
|||
addons-linter my-addon/src/
|
||||
```
|
||||
|
||||
The addons-linter will check your add-on and show you errors, warnings,
|
||||
and friendly messages for your add-on. If you want more info on the options
|
||||
you can enable/disable for the command-line app, use the `--help` option:
|
||||
The addons-linter will check your add-on and show you errors, warnings, and friendly messages for your add-on. If you want more info on the options you can enable/disable for the command-line app, use the `--help` option:
|
||||
|
||||
```sh
|
||||
addons-linter --help
|
||||
|
@ -106,13 +99,11 @@ linter.run()
|
|||
|
||||
## Development
|
||||
|
||||
If you'd like to help us develop the addons-linter, that's great! It's
|
||||
pretty easy to get started, you just need node.js installed on your machine.
|
||||
If you'd like to help us develop the addons-linter, that's great! It's pretty easy to get started, you just need node.js installed on your machine.
|
||||
|
||||
### Quick Start
|
||||
|
||||
If you have node.js installed, here's the quick start to getting
|
||||
your development dependencies installed and running the tests
|
||||
If you have node.js installed, here's the quick start to getting your development dependencies installed and running the tests
|
||||
|
||||
```sh
|
||||
git clone https://github.com/mozilla/addons-linter.git
|
||||
|
@ -136,9 +127,7 @@ bin/addons-linter my-addon.zip
|
|||
|
||||
addons-linter requires node.js v8 or greater. Have a look at our `.travis.yml` file which node.js versions we officially test.
|
||||
|
||||
Using nvm is probably the easiest way to manage multiple node versions side by side. See
|
||||
[nvm on github](https://github.com/creationix/nvm) for more details.
|
||||
|
||||
Using nvm is probably the easiest way to manage multiple node versions side by side. See [nvm on github](https://github.com/creationix/nvm) for more details.
|
||||
|
||||
### Install dependencies
|
||||
|
||||
|
@ -152,15 +141,17 @@ Dependencies are automatically kept up-to-date using [greenkeeper](http://greenk
|
|||
|
||||
#### npm scripts
|
||||
|
||||
| Script | Description |
|
||||
|-------------------------|----------------------------------------------------|
|
||||
| npm test | Runs the tests (watches for changes) |
|
||||
| npm test-coverage | Runs the tests with coverage (watches for changes) |
|
||||
| npm test-once | Runs the tests once |
|
||||
| npm lint | Runs eslint |
|
||||
| npm test-coverage-once | Runs the tests once with coverage |
|
||||
| test-integration-linter | Runs our integration test-suite |
|
||||
| npm [run] build | Builds the lib (used by Travis) |
|
||||
| Script | Description |
|
||||
| ------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| npm test | Runs the tests (watches for changes) |
|
||||
| npm [run] build | Builds the lib (used by Travis) |
|
||||
| npm run test-coverage | Runs the tests with coverage (watches for changes) |
|
||||
| npm run test-once | Runs the tests once |
|
||||
| npm run lint | Runs eslint |
|
||||
| npm run test-coverage-once | Runs the tests once with coverage |
|
||||
| npm run test-integration-linter | Runs our integration test-suite |
|
||||
| npm run prettier | Automatically format the whole code-base with Prettier |
|
||||
| npm run prettier-dev | Automatically compare and format modified source files against the master branch |
|
||||
|
||||
### Building
|
||||
|
||||
|
@ -174,13 +165,11 @@ Run `npm test`. This will watch for file-changes and re-runs the test suite.
|
|||
|
||||
#### Coverage
|
||||
|
||||
We're looking to maintain coverage at 100%. Use the coverage data in the
|
||||
test output to work out what lines aren't covered and ensure they're covered.
|
||||
We're looking to maintain coverage at 100%. Use the coverage data in the test output to work out what lines aren't covered and ensure they're covered.
|
||||
|
||||
#### Assertions and testing APIs
|
||||
|
||||
We are using using sinon for assertions, mocks, stubs and more [see the Sinon docs for the API
|
||||
available](http://sinonjs.org/).
|
||||
We are using using sinon for assertions, mocks, stubs and more [see the Sinon docs for the API available](http://sinonjs.org/).
|
||||
|
||||
[Jest](https://facebook.github.io/jest/) is being used as a test-runner but also provides helpful tools. Please make sure you read their documentation for more details.
|
||||
|
||||
|
@ -188,61 +177,47 @@ available](http://sinonjs.org/).
|
|||
|
||||
We use [pino](https://github.com/pinojs/pino) for logging:
|
||||
|
||||
* By default logging is off (level is set to 'fatal') .
|
||||
* Logging in tests can be enabled using an env var e.g: `LOG_LEVEL=debug jest test`
|
||||
* Logging on the cli can be enabled with `--log-level [level]`.
|
||||
- By default logging is off (level is set to 'fatal') .
|
||||
- Logging in tests can be enabled using an env var e.g: `LOG_LEVEL=debug jest test`
|
||||
- Logging on the cli can be enabled with `--log-level [level]`.
|
||||
|
||||
### Prettier
|
||||
|
||||
We use [Prettier](https://prettier.io/) to automatically format our JavaScript code and stop all the on-going debates over styles. As a developer, you have to run it (with `npm run prettier-dev`) before submitting a Pull Request.
|
||||
|
||||
## Architecture
|
||||
|
||||
In a nutshell the way the linter works is to take an add-on
|
||||
package, extract the metadata from the xpi (zip) format and then
|
||||
process the files it finds through various content scanners.
|
||||
In a nutshell the way the linter works is to take an add-on package, extract the metadata from the xpi (zip) format and then process the files it finds through various content scanners.
|
||||
|
||||
We are heavily relying on [Eslint](https://eslint.org/) for JavaScript linting, [cheerio](https://github.com/cheeriojs/cheerio) for HTML parsing as well as [fluent.js](https://github.com/projectfluent/fluent.js) for parsing language packs.
|
||||
|
||||
### Scanners
|
||||
|
||||
Each file-type has a scanner. For example: CSS files use `CSSScanner`;
|
||||
Javascript files use `JavaScriptScanner`. Each scanner looks at relevant
|
||||
files and passes each file through a parser which then hands off to
|
||||
a set of rules that look for specific things.
|
||||
Each file-type has a scanner. For example: CSS files use `CSSScanner`; Javascript files use `JavaScriptScanner`. Each scanner looks at relevant files and passes each file through a parser which then hands off to a set of rules that look for specific things.
|
||||
|
||||
### Rules
|
||||
|
||||
Rules get exported via a single function in a single file. A rule can
|
||||
have private functions it uses internally, but rule code should not depend
|
||||
on another rule file and each rule file should export one rule.
|
||||
Rules get exported via a single function in a single file. A rule can have private functions it uses internally, but rule code should not depend on another rule file and each rule file should export one rule.
|
||||
|
||||
Each rule function is passed data from the scanner in order to carry
|
||||
out the specific checks for that rule it returns a list of objects which
|
||||
are then made into message objects and are passed to the Collector.
|
||||
Each rule function is passed data from the scanner in order to carry out the specific checks for that rule it returns a list of objects which are then made into message objects and are passed to the Collector.
|
||||
|
||||
### Collector
|
||||
|
||||
The Collector is an in-memory store for all validation message objects
|
||||
"collected" as the contents of the package are processed.
|
||||
The Collector is an in-memory store for all validation message objects "collected" as the contents of the package are processed.
|
||||
|
||||
### Messages
|
||||
|
||||
Each message has a code which is also its key. It has a message which
|
||||
is a short outline of what the message represents, and a description
|
||||
which is more detail into why that message was logged. The type of
|
||||
the message is set as messages are added so that if necessary the
|
||||
same message could be an error *or* a warning for example.
|
||||
Each message has a code which is also its key. It has a message which is a short outline of what the message represents, and a description which is more detail into why that message was logged. The type of the message is set as messages are added so that if necessary the same message could be an error _or_ a warning for example.
|
||||
|
||||
### Output
|
||||
|
||||
Lastly when the processing is complete the linter will output
|
||||
the collected data as text or JSON.
|
||||
Lastly when the processing is complete the linter will output the collected data as text or JSON.
|
||||
|
||||
## Deploys
|
||||
|
||||
We deploy to npm automatically using TravisCI. To release a new version,
|
||||
increment the version in `package.json` and create a PR. Make sure your
|
||||
version number conforms to the [semver][] format eg: `0.2.1`.
|
||||
We deploy to npm automatically using TravisCI. To release a new version, increment the version in `package.json` and create a PR. Make sure your version number conforms to the [semver][] format eg: `0.2.1`.
|
||||
|
||||
After merging the PR, [create a new release][new release] with the same tag
|
||||
name as your new version. Once the build passes it will deploy. Magic! ✨
|
||||
After merging the PR, [create a new release][new release] with the same tag name as your new version. Once the build passes it will deploy. Magic! ✨
|
||||
|
||||
[new release]: https://github.com/mozilla/addons-linter/releases/new
|
||||
[semver]: http://semver.org/
|
||||
|
|
|
@ -8,13 +8,20 @@ global.localesRoot = path.join(absoluteAppRoot, 'dist', 'locale');
|
|||
global.nodeRequire = require;
|
||||
|
||||
try {
|
||||
// `../dist/addons-linter` doesn't exist if the linter has not been
|
||||
// built yet. Disable this eslinting so that users don't get confused
|
||||
// needlessly and aren't required to build the linter for eslint
|
||||
// to pass.
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
require('../dist/addons-linter')
|
||||
.createInstance({ runAsBinary: true })
|
||||
.run();
|
||||
} catch (err) {
|
||||
if (err.code === 'MODULE_NOT_FOUND') {
|
||||
console.log('You did not build addons-linter yet.');
|
||||
console.log('Please run `npm install` and `npm run build` or see README.md for more information.');
|
||||
console.log(
|
||||
'Please run `npm install` and `npm run build` or see README.md for more information.'
|
||||
);
|
||||
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
|
|
|
@ -31,5 +31,7 @@ poFiles.forEach((pofile) => {
|
|||
const localeObject = JSON.parse(json);
|
||||
|
||||
fs.writeFileSync(
|
||||
localeOutputFilePath, `module.exports = ${toSource(localeObject)}`);
|
||||
localeOutputFilePath,
|
||||
`module.exports = ${toSource(localeObject)}`
|
||||
);
|
||||
});
|
||||
|
|
|
@ -11,7 +11,9 @@ md.use(emoji);
|
|||
md.use(markdownItAnchor, {
|
||||
permalink: true,
|
||||
});
|
||||
const markdown = md.render(fs.readFileSync('docs/rules.md', { encoding: 'utf8' }));
|
||||
const markdown = md.render(
|
||||
fs.readFileSync('docs/rules.md', { encoding: 'utf8' })
|
||||
);
|
||||
const template = fs.readFileSync('docs/rules.tmpl', { encoding: 'utf8' });
|
||||
const html = template.replace('{{MARKDOWN}}', markdown);
|
||||
fs.writeFileSync('docs/html/index.html', html);
|
||||
|
|
|
@ -27,6 +27,8 @@ for (let i = 0; i < supportedLangs.length; i++) {
|
|||
continue; // eslint-disable-line no-continue
|
||||
}
|
||||
|
||||
shell.exec(`msginit --no-translator --input=${templateDir}/messages.pot
|
||||
--output-file=${outputFile} -l ${locale}`.replace('\n', ' '));
|
||||
shell.exec(
|
||||
`msginit --no-translator --input=${templateDir}/messages.pot
|
||||
--output-file=${outputFile} -l ${locale}`.replace('\n', ' ')
|
||||
);
|
||||
}
|
||||
|
|
|
@ -41,8 +41,8 @@ try {
|
|||
emptyDir(importedDir, /.*\.json$/);
|
||||
emptyDir(importedDir, /index\.js$/);
|
||||
|
||||
schemaImport.fetchSchemas(
|
||||
{ inputPath: filePath, outputPath: firefoxDir })
|
||||
schemaImport
|
||||
.fetchSchemas({ inputPath: filePath, outputPath: firefoxDir })
|
||||
.then(() => {
|
||||
schemaImport.importSchemas(firefoxDir, updatesDir, importedDir);
|
||||
})
|
||||
|
|
|
@ -28,13 +28,17 @@ function publish() {
|
|||
name: process.env.GH_USER,
|
||||
email: process.env.GH_EMAIL,
|
||||
},
|
||||
repo: 'https://' + process.env.GH_TOKEN + '@github.com/mozilla/addons-linter.git', // eslint-disable-line
|
||||
repo: `https://${
|
||||
process.env.GH_TOKEN
|
||||
}@github.com/mozilla/addons-linter.git`,
|
||||
message: `Publish rules (auto)${getDeployMessage()}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.TRAVIS_SECURE_ENV_VARS === 'true' &&
|
||||
process.env.TRAVIS_PULL_REQUEST === 'false') {
|
||||
if (
|
||||
process.env.TRAVIS_SECURE_ENV_VARS === 'true' &&
|
||||
process.env.TRAVIS_PULL_REQUEST === 'false'
|
||||
) {
|
||||
console.log('Pushing branch for docker build');
|
||||
publish();
|
||||
} else {
|
||||
|
|
186
docs/rules.md
186
docs/rules.md
|
@ -1,124 +1,122 @@
|
|||
# Linter Rules
|
||||
|
||||
This document is automatically published on [github pages](http://mozilla.github.io/addons-linter/).
|
||||
To update it edit `docs/rules.md` in the
|
||||
[github repo](https://github.com/mozilla/addons-linter).
|
||||
This document is automatically published on [github pages](http://mozilla.github.io/addons-linter/). To update it edit `docs/rules.md` in the [github repo](https://github.com/mozilla/addons-linter).
|
||||
|
||||
Rules are sorted by severity.
|
||||
|
||||
## JavaScript
|
||||
|
||||
| Message code | Severity | Description |
|
||||
| ----------------------------|---------------|-------------------------------------|
|
||||
| `KNOWN_LIBRARY` | notice | This is version of a JS library is known and generally accepted. |
|
||||
| `OPENDIALOG_NONLIT_URI` | notice | openDialog called with non-literal parameter |
|
||||
| `EVENT_LISTENER_FOURTH` | notice | `addEventListener` called with truthy fourth argument |
|
||||
| `UNEXPECTED_GLOGAL_ARG` | warning | Unexpected global passed as an argument |
|
||||
| `NO_IMPLIED_EVAL` | warning | disallow the use of `eval()`-like methods |
|
||||
| `OPENDIALOG_REMOTE_URI` | warning | openDialog called with non-local URI |
|
||||
| `NO_DOCUMENT_WRITE` | warning | Use of `document.write` strongly discouraged. |
|
||||
| `JS_SYNTAX_ERROR` | warning | JavaScript compile-time error |
|
||||
| `UNADVISED_LIBRARY` | warning | This version of a JS library is not recommended. |
|
||||
| `TABS_GETSELECTED` | warning | Deprecated API tabs.getSelected |
|
||||
| `TABS_SENDREQUEST` | warning | Deprecated API tabs.sendRequest |
|
||||
| `TABS_GETALLINWINDOW` | warning | Deprecated API tabs.getAllInWindow |
|
||||
| `TABS_ONSELECTIONCHANGED` | warning | Deprecated API tabs.onSelectionChanged |
|
||||
| `TABS_ONACTIVECHANGED` | warning | Deprecated API tabs.onActiveChanged |
|
||||
| `EXT_SENDREQUEST` | warning | Deprecated API extension.sendRequest |
|
||||
| `EXT_ONREQUESTEXTERNAL` | warning | Deprecated API extension.onRequestExternal |
|
||||
| `EXT_ONREQUEST` | warning | Deprecated API extension.onRequest |
|
||||
| `APP_GETDETAILS` | warning | Deprecated API app.getDetails |
|
||||
| `STORAGE_LOCAL` | warning | Temporary IDs can cause issues with storage.local |
|
||||
| `STORAGE_SYNC` | warning | Temporary IDs can cause issues with storage.sync |
|
||||
| `IDENTITY_GETREDIRECTURL` | warning | Temporary IDs can cause issues with identity.getRedirectURL |
|
||||
| `BANNED_LIBRARY` | error | This version of a JS library is banned for security reasons. |
|
||||
| Message code | Severity | Description |
|
||||
| ------------------------- | -------- | ---------------------------------------------------------------- |
|
||||
| `KNOWN_LIBRARY` | notice | This is version of a JS library is known and generally accepted. |
|
||||
| `OPENDIALOG_NONLIT_URI` | notice | openDialog called with non-literal parameter |
|
||||
| `EVENT_LISTENER_FOURTH` | notice | `addEventListener` called with truthy fourth argument |
|
||||
| `UNEXPECTED_GLOGAL_ARG` | warning | Unexpected global passed as an argument |
|
||||
| `NO_IMPLIED_EVAL` | warning | disallow the use of `eval()`-like methods |
|
||||
| `OPENDIALOG_REMOTE_URI` | warning | openDialog called with non-local URI |
|
||||
| `NO_DOCUMENT_WRITE` | warning | Use of `document.write` strongly discouraged. |
|
||||
| `JS_SYNTAX_ERROR` | warning | JavaScript compile-time error |
|
||||
| `UNADVISED_LIBRARY` | warning | This version of a JS library is not recommended. |
|
||||
| `TABS_GETSELECTED` | warning | Deprecated API tabs.getSelected |
|
||||
| `TABS_SENDREQUEST` | warning | Deprecated API tabs.sendRequest |
|
||||
| `TABS_GETALLINWINDOW` | warning | Deprecated API tabs.getAllInWindow |
|
||||
| `TABS_ONSELECTIONCHANGED` | warning | Deprecated API tabs.onSelectionChanged |
|
||||
| `TABS_ONACTIVECHANGED` | warning | Deprecated API tabs.onActiveChanged |
|
||||
| `EXT_SENDREQUEST` | warning | Deprecated API extension.sendRequest |
|
||||
| `EXT_ONREQUESTEXTERNAL` | warning | Deprecated API extension.onRequestExternal |
|
||||
| `EXT_ONREQUEST` | warning | Deprecated API extension.onRequest |
|
||||
| `APP_GETDETAILS` | warning | Deprecated API app.getDetails |
|
||||
| `STORAGE_LOCAL` | warning | Temporary IDs can cause issues with storage.local |
|
||||
| `STORAGE_SYNC` | warning | Temporary IDs can cause issues with storage.sync |
|
||||
| `IDENTITY_GETREDIRECTURL` | warning | Temporary IDs can cause issues with identity.getRedirectURL |
|
||||
| `BANNED_LIBRARY` | error | This version of a JS library is banned for security reasons. |
|
||||
|
||||
## Markup
|
||||
|
||||
### CSS
|
||||
|
||||
| Message code | Severity | Description |
|
||||
| ----------------------------|-----------|-------------------------------------|
|
||||
| `CSS_SYNTAX_ERROR` | error | A CSS syntax error was detected |
|
||||
| `INVALID_SELECTOR_NESTING` | error | CSS selectors should not be nested |
|
||||
| Message code | Severity | Description |
|
||||
| -------------------------- | -------- | ---------------------------------- |
|
||||
| `CSS_SYNTAX_ERROR` | error | A CSS syntax error was detected |
|
||||
| `INVALID_SELECTOR_NESTING` | error | CSS selectors should not be nested |
|
||||
|
||||
### HTML
|
||||
|
||||
| Message code | Severity | Description |
|
||||
| --------------------------|-----------|-------------------------------------|
|
||||
| `INLINE_SCRIPT` | warning | Inline script is disallowed by CSP |
|
||||
| `REMOTE_SCRIPT` | warning | Remote scripts are not allowed as per Add-on Policies |
|
||||
| Message code | Severity | Description |
|
||||
| --------------- | -------- | ----------------------------------------------------- |
|
||||
| `INLINE_SCRIPT` | warning | Inline script is disallowed by CSP |
|
||||
| `REMOTE_SCRIPT` | warning | Remote scripts are not allowed as per Add-on Policies |
|
||||
|
||||
## Content
|
||||
|
||||
| Message code | Severity | Description |
|
||||
| ------------------------|-----------|-------------------------------------|
|
||||
| `HIDDEN_FILE` | warning | Hidden file flagged |
|
||||
| `FLAGGED_FILE` | warning | Flagged filename found |
|
||||
| Message code | Severity | Description |
|
||||
| -------------- | -------- | ---------------------- |
|
||||
| `HIDDEN_FILE` | warning | Hidden file flagged |
|
||||
| `FLAGGED_FILE` | warning | Flagged filename found |
|
||||
|
||||
## Package layout
|
||||
|
||||
| Message code | Severity | Description |
|
||||
| ----------------------------|-----------|-------------------------------------|
|
||||
| `MOZILLA_COND_OF_USE` | notice | Mozilla conditions of use violation |
|
||||
| `FLAGGED_FILE_TYPE` | notice | (Binary) Flagged file type found |
|
||||
| `FLAGGED_FILE_EXTENSION` | warning | Flagged file extensions found |
|
||||
| `DUPLICATE_XPI_ENTRY` | warning | Package contains duplicate entries |
|
||||
| `ALREADY_SIGNED` | warning | Already signed |
|
||||
| `COINMINER_USAGE_DETECTED` | warning | Firefox add-ons are not allowed to run coin miners |
|
||||
| `BAD_ZIPFILE` | error | Bad zip file |
|
||||
| `FILE_TOO_LARGE` | error | File is too large to parse |
|
||||
| Message code | Severity | Description |
|
||||
| -------------------------- | -------- | -------------------------------------------------- |
|
||||
| `MOZILLA_COND_OF_USE` | notice | Mozilla conditions of use violation |
|
||||
| `FLAGGED_FILE_TYPE` | notice | (Binary) Flagged file type found |
|
||||
| `FLAGGED_FILE_EXTENSION` | warning | Flagged file extensions found |
|
||||
| `DUPLICATE_XPI_ENTRY` | warning | Package contains duplicate entries |
|
||||
| `ALREADY_SIGNED` | warning | Already signed |
|
||||
| `COINMINER_USAGE_DETECTED` | warning | Firefox add-ons are not allowed to run coin miners |
|
||||
| `BAD_ZIPFILE` | error | Bad zip file |
|
||||
| `FILE_TOO_LARGE` | error | File is too large to parse |
|
||||
|
||||
## Type detection
|
||||
|
||||
| Message code | Severity | Description |
|
||||
| ----------------------------|-----------|-------------------------------------|
|
||||
| `TYPE_NO_MANIFEST_JSON` | notice | Add-on missing manifest_json for type detection |
|
||||
| Message code | Severity | Description |
|
||||
| ----------------------- | -------- | ----------------------------------------------- |
|
||||
| `TYPE_NO_MANIFEST_JSON` | notice | Add-on missing manifest_json for type detection |
|
||||
|
||||
## Language packs
|
||||
|
||||
| Message code | Severity | Description |
|
||||
| ----------------------------|-----------|-------------------------------------|
|
||||
| FLUENT_INVALID | warning | Invalid fluent template file |
|
||||
| Message code | Severity | Description |
|
||||
| -------------- | -------- | ---------------------------- |
|
||||
| FLUENT_INVALID | warning | Invalid fluent template file |
|
||||
|
||||
## Web Extensions / manifest.json
|
||||
|
||||
| Message code | Severity | Description |
|
||||
| ----------------------------|---------------|-------------------------------------|
|
||||
| `MANIFEST_UNUSED_UPDATE` | notice | update_url ignored in manifest.json |
|
||||
| `PROP_VERSION_TOOLKIT_ONLY` | notice | version is in the toolkit format in manifest.json |
|
||||
| `CORRUPT_ICON_FILE` | warning | Icons must not be corrupt |
|
||||
| `MANIFEST_CSP` | warning | content_security_policy in manifest.json means more review |
|
||||
| `MANIFEST_CSP_UNSAFE_EVAL` | warning | usage of 'unsafe-eval' is strongly discouraged |
|
||||
| `MANIFEST_PERMISSIONS` | warning | Unknown permission |
|
||||
| `NO_MESSAGES_FILE` | warning | When default_locale is specified a matching messages.json must exist |
|
||||
| `NO_DEFAULT_LOCALE` | warning | When _locales directory exists, default_locale must exist |
|
||||
| `UNSAFE_VAR_ASSIGNMENT` | warning | Assignment using dynamic, unsanitized values |
|
||||
| `UNSUPPORTED_API` | warning | Unsupported or unknown browser API |
|
||||
| `DANGEROUS_EVAL` | warning | `eval` and the `Function` constructor are discouraged |
|
||||
| `STRICT_MAX_VERSION` | warning | strict_max_version not required |
|
||||
| `PREDEFINED_MESSAGE_NAME` | warning | String name is reserved for a predefined |
|
||||
| `MISSING_PLACEHOLDER` | warning | Placeholder for message is not |
|
||||
| `WRONG_ICON_EXTENSION` | error | Icons must have valid extension |
|
||||
| `MANIFEST_UPDATE_URL` | error | update_url not allowed in manifest.json |
|
||||
| `MANIFEST_FIELD_REQUIRED` | error | A required field is missing |
|
||||
| `MANIFEST_FIELD_INVALID` | error | A field is invalid |
|
||||
| `MANIFEST_BAD_PERMISSION` | error | Bad permission |
|
||||
| `JSON_BLOCK_COMMENTS` | error | Block Comments are not allowed in JSON |
|
||||
| `MANIFEST_INVALID_CONTENT` | error | This add-on contains forbidden content |
|
||||
| `CONTENT_SCRIPT_NOT_FOUND` | error | Content script file could not be found |
|
||||
| `CONTENT_SCRIPT_EMPTY` | error | Content script file name should not be empty |
|
||||
| `NO_MESSAGE` | error | Translation string is missing the message |
|
||||
| `INVALID_MESSAGE_NAME` | error | String name contains invalid characters |
|
||||
| `INVALID_PLACEHOLDER_NAME` | error | Placeholder name contains invalid characters |
|
||||
| `NO_PLACEHOLDER_CONTENT` | error | Placeholder is missing the content |
|
||||
| `JSON_INVALID` | error | JSON is not well formed |
|
||||
| `JSON_DUPLICATE_KEY` | error | Duplicate key in JSON |
|
||||
| `MANIFEST_VERSION_INVALID` | error | manifest_version in manifest.json is not valid. |
|
||||
| `PROP_NAME_MISSING` | error | name property missing from manifest.json |
|
||||
| `PROP_NAME_INVALID` | error | name property is invalid in manifest.json |
|
||||
| `PROP_VERSION_MISSING` | error | version property missing from manifest.json |
|
||||
| `PROP_VERSION_INVALID` | error | version is invalid in manifest.json |
|
||||
| `MANIFEST_DICT_NOT_FOUND` | error | A dictionary file defined in the manifest could not be found |
|
||||
| `MANIFEST_MULTIPLE_DICTS` | error | Multiple dictionaries found |
|
||||
| `MANIFEST_EMPTY_DICTS` | error | Empty `dictionaries` object |
|
||||
| Message code | Severity | Description |
|
||||
| --------------------------- | -------- | -------------------------------------------------------------------- |
|
||||
| `MANIFEST_UNUSED_UPDATE` | notice | update_url ignored in manifest.json |
|
||||
| `PROP_VERSION_TOOLKIT_ONLY` | notice | version is in the toolkit format in manifest.json |
|
||||
| `CORRUPT_ICON_FILE` | warning | Icons must not be corrupt |
|
||||
| `MANIFEST_CSP` | warning | content_security_policy in manifest.json means more review |
|
||||
| `MANIFEST_CSP_UNSAFE_EVAL` | warning | usage of 'unsafe-eval' is strongly discouraged |
|
||||
| `MANIFEST_PERMISSIONS` | warning | Unknown permission |
|
||||
| `NO_MESSAGES_FILE` | warning | When default_locale is specified a matching messages.json must exist |
|
||||
| `NO_DEFAULT_LOCALE` | warning | When \_locales directory exists, default_locale must exist |
|
||||
| `UNSAFE_VAR_ASSIGNMENT` | warning | Assignment using dynamic, unsanitized values |
|
||||
| `UNSUPPORTED_API` | warning | Unsupported or unknown browser API |
|
||||
| `DANGEROUS_EVAL` | warning | `eval` and the `Function` constructor are discouraged |
|
||||
| `STRICT_MAX_VERSION` | warning | strict_max_version not required |
|
||||
| `PREDEFINED_MESSAGE_NAME` | warning | String name is reserved for a predefined |
|
||||
| `MISSING_PLACEHOLDER` | warning | Placeholder for message is not |
|
||||
| `WRONG_ICON_EXTENSION` | error | Icons must have valid extension |
|
||||
| `MANIFEST_UPDATE_URL` | error | update_url not allowed in manifest.json |
|
||||
| `MANIFEST_FIELD_REQUIRED` | error | A required field is missing |
|
||||
| `MANIFEST_FIELD_INVALID` | error | A field is invalid |
|
||||
| `MANIFEST_BAD_PERMISSION` | error | Bad permission |
|
||||
| `JSON_BLOCK_COMMENTS` | error | Block Comments are not allowed in JSON |
|
||||
| `MANIFEST_INVALID_CONTENT` | error | This add-on contains forbidden content |
|
||||
| `CONTENT_SCRIPT_NOT_FOUND` | error | Content script file could not be found |
|
||||
| `CONTENT_SCRIPT_EMPTY` | error | Content script file name should not be empty |
|
||||
| `NO_MESSAGE` | error | Translation string is missing the message |
|
||||
| `INVALID_MESSAGE_NAME` | error | String name contains invalid characters |
|
||||
| `INVALID_PLACEHOLDER_NAME` | error | Placeholder name contains invalid characters |
|
||||
| `NO_PLACEHOLDER_CONTENT` | error | Placeholder is missing the content |
|
||||
| `JSON_INVALID` | error | JSON is not well formed |
|
||||
| `JSON_DUPLICATE_KEY` | error | Duplicate key in JSON |
|
||||
| `MANIFEST_VERSION_INVALID` | error | manifest_version in manifest.json is not valid. |
|
||||
| `PROP_NAME_MISSING` | error | name property missing from manifest.json |
|
||||
| `PROP_NAME_INVALID` | error | name property is invalid in manifest.json |
|
||||
| `PROP_VERSION_MISSING` | error | version property missing from manifest.json |
|
||||
| `PROP_VERSION_INVALID` | error | version is invalid in manifest.json |
|
||||
| `MANIFEST_DICT_NOT_FOUND` | error | A dictionary file defined in the manifest could not be found |
|
||||
| `MANIFEST_MULTIPLE_DICTS` | error | Multiple dictionaries found |
|
||||
| `MANIFEST_EMPTY_DICTS` | error | Empty `dictionaries` object |
|
||||
|
|
|
@ -1,53 +1,48 @@
|
|||
# Add-on Type Support [DRAFT]
|
||||
|
||||
Going forward the amo-validator will continue to be the linter for legacy addo-ns.
|
||||
This document lists what types of addons the addon-linter will support and
|
||||
provides somde details as to the scope of the features needed.
|
||||
Going forward the amo-validator will continue to be the linter for legacy addo-ns. This document lists what types of addons the addon-linter will support and provides somde details as to the scope of the features needed.
|
||||
|
||||
* Web Extensions
|
||||
* Dictionaries
|
||||
* Language Packs
|
||||
* Search Add-ons
|
||||
- Web Extensions
|
||||
- Dictionaries
|
||||
- Language Packs
|
||||
- Search Add-ons
|
||||
|
||||
## Web Extensions
|
||||
|
||||
Required features for linting:
|
||||
|
||||
* Validation of the `manifest.json`. Swtich to JSON scheme for this.
|
||||
* Look into rules for to guard against inadvertent privilege
|
||||
escalation holes. This type of issue would come from a site exploiting an
|
||||
extension.
|
||||
* js validation of content scripts (Rules need TBD)
|
||||
* Flag un-approved libs based on file name that match libs
|
||||
* Skip JS linting on libs that match the approved list.
|
||||
* Rules for specific APIs.
|
||||
- Validation of the `manifest.json`. Swtich to JSON scheme for this.
|
||||
- Look into rules for to guard against inadvertent privilege escalation holes. This type of issue would come from a site exploiting an extension.
|
||||
- js validation of content scripts (Rules need TBD)
|
||||
- Flag un-approved libs based on file name that match libs
|
||||
- Skip JS linting on libs that match the approved list.
|
||||
- Rules for specific APIs.
|
||||
|
||||
Nice to haves:
|
||||
|
||||
* Deal with aiding ports from Google Chrome + Opera.
|
||||
* Determine if an API being used is missing a permission
|
||||
* Determine if a Permission is begin asked for unnecessarily.
|
||||
- Deal with aiding ports from Google Chrome + Opera.
|
||||
- Determine if an API being used is missing a permission
|
||||
- Determine if a Permission is begin asked for unnecessarily.
|
||||
|
||||
Docs: https://developer.mozilla.org/Add-ons/WebExtensions
|
||||
|
||||
## Dictionaries
|
||||
|
||||
* Deal with any dictionary specific rules.
|
||||
- Deal with any dictionary specific rules.
|
||||
|
||||
Docs: https://developer.mozilla.org/docs/Creating_a_spell_check_dictionary_add-on
|
||||
|
||||
## Lang Packs
|
||||
|
||||
* Validate `chrome.manifest`
|
||||
* Deal with any other langpack specific rules.
|
||||
- Validate `chrome.manifest`
|
||||
- Deal with any other langpack specific rules.
|
||||
|
||||
Docs appear to be a bit thin on the ground for these. They need looking
|
||||
into further and we should look at the existing rules.
|
||||
Docs appear to be a bit thin on the ground for these. They need looking into further and we should look at the existing rules.
|
||||
|
||||
## Search Addons
|
||||
|
||||
What's needed?
|
||||
|
||||
* Validation of the opensearch xml
|
||||
- Validation of the opensearch xml
|
||||
|
||||
Old code is here https://github.com/mozilla/amo-validator/blob/master/validator/opensearch.py
|
||||
|
|
|
@ -5,14 +5,8 @@ module.exports = {
|
|||
'<rootDir>/config/',
|
||||
],
|
||||
collectCoverageFrom: ['src/**/*.js'],
|
||||
moduleDirectories: [
|
||||
'src',
|
||||
'node_modules',
|
||||
],
|
||||
moduleFileExtensions: [
|
||||
'js',
|
||||
'json',
|
||||
],
|
||||
moduleDirectories: ['src', 'node_modules'],
|
||||
moduleFileExtensions: ['js', 'json'],
|
||||
moduleNameMapper: {
|
||||
// Alias tests for tests to be able to import helpers.
|
||||
'^tests/(.*)$': '<rootDir>/tests/$1',
|
||||
|
@ -31,9 +25,7 @@ module.exports = {
|
|||
'^.+\\.js$': 'babel-jest',
|
||||
'^.+\\.txt$': 'jest-raw-loader',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'<rootDir>/node_modules/',
|
||||
],
|
||||
transformIgnorePatterns: ['<rootDir>/node_modules/'],
|
||||
testEnvironment: 'node',
|
||||
verbose: false,
|
||||
};
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
const config = require('./jest.config');
|
||||
|
||||
|
||||
module.exports = Object.assign({}, config, {
|
||||
testMatch: [
|
||||
'<rootDir>/**/integration(*).js?(x)',
|
||||
],
|
||||
testMatch: ['<rootDir>/**/integration(*).js?(x)'],
|
||||
});
|
||||
|
|
|
@ -3560,44 +3560,57 @@
|
|||
}
|
||||
},
|
||||
"eslint-config-airbnb": {
|
||||
"version": "16.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-16.1.0.tgz",
|
||||
"integrity": "sha512-zLyOhVWhzB/jwbz7IPSbkUuj7X2ox4PHXTcZkEmDqTvd0baJmJyuxlFPDlZOE/Y5bC+HQRaEkT3FoHo9wIdRiw==",
|
||||
"version": "17.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-17.1.0.tgz",
|
||||
"integrity": "sha512-R9jw28hFfEQnpPau01NO5K/JWMGLi6aymiF6RsnMURjTk+MqZKllCqGK/0tOvHkPi/NWSSOU2Ced/GX++YxLnw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eslint-config-airbnb-base": "12.1.0"
|
||||
"eslint-config-airbnb-base": "13.1.0",
|
||||
"object.assign": "4.1.0",
|
||||
"object.entries": "1.0.4"
|
||||
}
|
||||
},
|
||||
"eslint-config-airbnb-base": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-12.1.0.tgz",
|
||||
"integrity": "sha512-/vjm0Px5ZCpmJqnjIzcFb9TKZrKWz0gnuG/7Gfkt0Db1ELJR51xkZth+t14rYdqWgX836XbuxtArbIHlVhbLBA==",
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-13.1.0.tgz",
|
||||
"integrity": "sha512-XWwQtf3U3zIoKO1BbHh6aUhJZQweOwSt4c2JrPDg9FP3Ltv3+YfEv7jIDB8275tVnO/qOHbfuYg3kzw6Je7uWw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eslint-restricted-globals": "0.1.1"
|
||||
"eslint-restricted-globals": "0.1.1",
|
||||
"object.assign": "4.1.0",
|
||||
"object.entries": "1.0.4"
|
||||
}
|
||||
},
|
||||
"eslint-config-amo": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-amo/-/eslint-config-amo-1.7.0.tgz",
|
||||
"integrity": "sha1-KltRdJl7ABMjFQaS3o0CqT7LLEc=",
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-amo/-/eslint-config-amo-1.8.3.tgz",
|
||||
"integrity": "sha512-mKT6/5YT52SDWQwUDYPWa2zkbFLrGlvw6Qq2PUHkUBSb7LdYsTDMPekap9sTvMZ6R6sP4WamoH5cROEMGFni3w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eslint-config-airbnb": "16.1.0",
|
||||
"eslint-plugin-import": "2.8.0",
|
||||
"eslint-plugin-jest": "21.13.0",
|
||||
"eslint-plugin-jsx-a11y": "6.1.0",
|
||||
"eslint-plugin-react": "7.5.1"
|
||||
"eslint-config-airbnb": "17.1.0",
|
||||
"eslint-plugin-import": "2.14.0",
|
||||
"eslint-plugin-jest": "21.18.0",
|
||||
"eslint-plugin-jsx-a11y": "6.1.1",
|
||||
"eslint-plugin-react": "7.11.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint-plugin-jest": {
|
||||
"version": "21.13.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-21.13.0.tgz",
|
||||
"integrity": "sha512-tR8bn4tZk1I5i6HstvucC5tprxJKeuyh0baP7xYasQq/RS/hPcmAgUE42d52R1ysg719OPKsFqkJDuyvNeolvg==",
|
||||
"version": "21.18.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-21.18.0.tgz",
|
||||
"integrity": "sha512-fhuJuehoMtuEQ3Klgx0629hDmbs0M0g4tSZ65Wq2NqpLWCK5UC7hQnGS1Wh4+Vc/9P4ss4HxqZ1XK7honqUZNg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint-config-prettier": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-3.0.1.tgz",
|
||||
"integrity": "sha512-vA0TB8HCx/idHXfKHYcg9J98p0Q8nkfNwNAoP7e+ywUidn6ScaFS5iqncZAHPz+/a0A/tp657ulFHFx/2JDP4Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"get-stdin": "6.0.0"
|
||||
}
|
||||
},
|
||||
"eslint-import-resolver-node": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz",
|
||||
|
@ -3648,6 +3661,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"eslint-plugin-amo": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-amo/-/eslint-plugin-amo-1.7.0.tgz",
|
||||
"integrity": "sha512-w/Tz6lLE65RqGrMTGvWw2k4wNy8Sw/T3Z/IddVJpu+MkHm8aNF54ZP2hgQ5vEuP+w8OGi2oGbynioXDJ0MMx9Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"requireindex": "1.1.0",
|
||||
"string-natural-compare": "2.0.2"
|
||||
}
|
||||
},
|
||||
"eslint-plugin-async-await": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-async-await/-/eslint-plugin-async-await-0.0.0.tgz",
|
||||
|
@ -3655,21 +3678,21 @@
|
|||
"dev": true
|
||||
},
|
||||
"eslint-plugin-import": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.8.0.tgz",
|
||||
"integrity": "sha512-Rf7dfKJxZ16QuTgVv1OYNxkZcsu/hULFnC+e+w0Gzi6jMC3guQoWQgxYxc54IDRinlb6/0v5z/PxxIKmVctN+g==",
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.14.0.tgz",
|
||||
"integrity": "sha512-FpuRtniD/AY6sXByma2Wr0TXvXJ4nA/2/04VPlfpmUDPOpOY264x+ILiwnrk/k4RINgDAyFZByxqPUbSQ5YE7g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"builtin-modules": "1.1.1",
|
||||
"contains-path": "0.1.0",
|
||||
"debug": "2.6.9",
|
||||
"doctrine": "1.5.0",
|
||||
"eslint-import-resolver-node": "0.3.2",
|
||||
"eslint-module-utils": "2.2.0",
|
||||
"has": "1.0.3",
|
||||
"lodash.cond": "4.5.2",
|
||||
"lodash": "4.17.10",
|
||||
"minimatch": "3.0.4",
|
||||
"read-pkg-up": "2.0.0"
|
||||
"read-pkg-up": "2.0.0",
|
||||
"resolve": "1.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"doctrine": {
|
||||
|
@ -3776,9 +3799,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"eslint-plugin-jsx-a11y": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.1.0.tgz",
|
||||
"integrity": "sha512-hnhf28u7Z9zlh7Y56tETrwnPeBvXgcqlP7ntHvZsWQs/n/p/vPnfNMNFWTqJAFcbd8PrDEifX1NRGHsjnUmqMw==",
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.1.1.tgz",
|
||||
"integrity": "sha512-JsxNKqa3TwmPypeXNnI75FntkUktGzI1wSa1LgNZdSOMI+B4sxnr1lSF8m8lPiz4mKiC+14ysZQM4scewUrP7A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"aria-query": "3.0.0",
|
||||
|
@ -4050,6 +4073,24 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"eslint-plugin-prettier": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-2.6.2.tgz",
|
||||
"integrity": "sha512-tGek5clmW5swrAx1mdPYM8oThrBE83ePh7LeseZHBWfHVGrHPhKn7Y5zgRMbU/9D5Td9K4CEmUPjGxA7iw98Og==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-diff": "1.1.2",
|
||||
"jest-docblock": "21.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"jest-docblock": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-21.2.0.tgz",
|
||||
"integrity": "sha512-5IZ7sY9dBAYSV+YjQ0Ovb540Ku7AO9Z5o2Cg789xj167iQuZ2cG+z0f3Uct6WeYLbU6aQiM2pCs7sZ+4dotydw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint-plugin-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.0.0.tgz",
|
||||
|
@ -4057,11 +4098,12 @@
|
|||
"dev": true
|
||||
},
|
||||
"eslint-plugin-react": {
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.5.1.tgz",
|
||||
"integrity": "sha512-YGSjB9Qu6QbVTroUZi66pYky3DfoIPLdHQ/wmrBGyBRnwxQsBXAov9j2rpXt/55i8nyMv6IRWJv2s4d4YnduzQ==",
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.11.1.tgz",
|
||||
"integrity": "sha512-cVVyMadRyW7qsIUh3FHp3u6QHNhOgVrLQYdQEB1bPWBsgbNCHdFAeNMquBMCcZJu59eNthX053L70l7gRt4SCw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-includes": "3.0.3",
|
||||
"doctrine": "2.1.0",
|
||||
"has": "1.0.3",
|
||||
"jsx-ast-utils": "2.0.1",
|
||||
|
@ -4409,6 +4451,12 @@
|
|||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
|
||||
},
|
||||
"fast-diff": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
|
||||
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
|
||||
"dev": true
|
||||
},
|
||||
"fast-json-parse": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-parse/-/fast-json-parse-1.0.3.tgz",
|
||||
|
@ -5227,6 +5275,12 @@
|
|||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz",
|
||||
"integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U="
|
||||
},
|
||||
"get-stdin": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
|
||||
"integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==",
|
||||
"dev": true
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
|
||||
|
@ -7694,12 +7748,6 @@
|
|||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
|
||||
},
|
||||
"lodash.cond": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz",
|
||||
"integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
|
@ -8125,6 +8173,12 @@
|
|||
"run-queue": "1.0.3"
|
||||
}
|
||||
},
|
||||
"mri": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.1.1.tgz",
|
||||
"integrity": "sha1-haom09ru7t+A3FmEr5XMXKXK2fE=",
|
||||
"dev": true
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
|
@ -8564,6 +8618,30 @@
|
|||
"isobject": "3.0.1"
|
||||
}
|
||||
},
|
||||
"object.assign": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
|
||||
"integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"define-properties": "1.1.2",
|
||||
"function-bind": "1.1.1",
|
||||
"has-symbols": "1.0.0",
|
||||
"object-keys": "1.0.12"
|
||||
}
|
||||
},
|
||||
"object.entries": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.0.4.tgz",
|
||||
"integrity": "sha1-G/mk3SKI9bM/Opk9JXZh8F0WGl8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"define-properties": "1.1.2",
|
||||
"es-abstract": "1.12.0",
|
||||
"function-bind": "1.1.1",
|
||||
"has": "1.0.3"
|
||||
}
|
||||
},
|
||||
"object.getownpropertydescriptors": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz",
|
||||
|
@ -9177,6 +9255,12 @@
|
|||
"integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=",
|
||||
"dev": true
|
||||
},
|
||||
"prettier": {
|
||||
"version": "1.14.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.14.2.tgz",
|
||||
"integrity": "sha512-McHPg0n1pIke+A/4VcaS2en+pTNjy4xF+Uuq86u/5dyDO59/TtFZtQ708QIRkEZ3qwKz3GVkVa6mpxK/CpB8Rg==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-format": {
|
||||
"version": "22.4.3",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-22.4.3.tgz",
|
||||
|
@ -9204,6 +9288,79 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"pretty-quick": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-1.6.0.tgz",
|
||||
"integrity": "sha512-bnCmsPy98ERD7VWBO+0y1OGWLfx/DPUjNFN2ZRVyxuGBiic1BXAGgjHsTKgBIbPISdqpP6KBEmRV0Lir4xu/BA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "2.4.0",
|
||||
"execa": "0.8.0",
|
||||
"find-up": "2.1.0",
|
||||
"ignore": "3.3.10",
|
||||
"mri": "1.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"execa": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz",
|
||||
"integrity": "sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cross-spawn": "5.1.0",
|
||||
"get-stream": "3.0.0",
|
||||
"is-stream": "1.1.0",
|
||||
"npm-run-path": "2.0.2",
|
||||
"p-finally": "1.0.0",
|
||||
"signal-exit": "3.0.2",
|
||||
"strip-eof": "1.0.0"
|
||||
}
|
||||
},
|
||||
"find-up": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
|
||||
"integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"locate-path": "2.0.0"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
|
||||
"integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-locate": "2.0.0",
|
||||
"path-exists": "3.0.0"
|
||||
}
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
|
||||
"integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-try": "1.0.0"
|
||||
}
|
||||
},
|
||||
"p-locate": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
|
||||
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-limit": "1.3.0"
|
||||
}
|
||||
},
|
||||
"p-try": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
|
||||
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"private": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
|
||||
|
@ -9830,6 +9987,12 @@
|
|||
"resolve-from": "1.0.1"
|
||||
}
|
||||
},
|
||||
"requireindex": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz",
|
||||
"integrity": "sha1-5UBLgVV+91225JxacgBIk/4D4WI=",
|
||||
"dev": true
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz",
|
||||
|
@ -10936,6 +11099,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"string-natural-compare": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-2.0.2.tgz",
|
||||
"integrity": "sha1-xc5OJ4q10SZa5vxVQ1rre3b8sAE=",
|
||||
"dev": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
|
||||
|
|
13
package.json
13
package.json
|
@ -19,6 +19,8 @@
|
|||
"test-integration-linter": "npm run test-integration -- tests/integration/addons-linter",
|
||||
"test-integration:production": "node tests/integration/run-as-production-env.js test-integration tests/integration/addons-linter",
|
||||
"lint": "npm run eslint",
|
||||
"prettier": "prettier --write '**'",
|
||||
"prettier-dev": "pretty-quick --branch master",
|
||||
"publish-rules": "bin/build-rules && cp node_modules/gfm.css/gfm.css docs/html/gfm.css && bin/publish-rules",
|
||||
"gen-contributing-toc": "doctoc CONTRIBUTING.md",
|
||||
"snyk-protect": "snyk protect",
|
||||
|
@ -65,6 +67,7 @@
|
|||
"relaxed-json": "1.0.1",
|
||||
"semver": "5.5.1",
|
||||
"shelljs": "0.8.2",
|
||||
"snyk": "^1.88.2",
|
||||
"source-map-support": "0.5.6",
|
||||
"strip-bom-stream": "3.0.0",
|
||||
"tosource": "1.0.0",
|
||||
|
@ -72,8 +75,7 @@
|
|||
"whatwg-url": "7.0.0",
|
||||
"xmldom": "0.1.27",
|
||||
"yargs": "12.0.1",
|
||||
"yauzl": "2.9.2",
|
||||
"snyk": "^1.88.2"
|
||||
"yauzl": "2.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "6.26.0",
|
||||
|
@ -93,9 +95,12 @@
|
|||
"comment-json": "1.1.3",
|
||||
"coveralls": "3.0.1",
|
||||
"doctoc": "1.3.1",
|
||||
"eslint-config-amo": "1.7.0",
|
||||
"eslint-config-amo": "1.8.3",
|
||||
"eslint-config-prettier": "3.0.1",
|
||||
"eslint-plugin-amo": "^1.7.0",
|
||||
"eslint-plugin-async-await": "0.0.0",
|
||||
"eslint-plugin-jest": "21.22.0",
|
||||
"eslint-plugin-prettier": "2.6.2",
|
||||
"eslint-plugin-promise": "4.0.0",
|
||||
"gfm.css": "1.1.2",
|
||||
"gh-pages": "^1.2.0",
|
||||
|
@ -110,6 +115,8 @@
|
|||
"markdown-it": "8.4.2",
|
||||
"markdown-it-anchor": "5.0.2",
|
||||
"markdown-it-emoji": "1.4.0",
|
||||
"prettier": "1.14.2",
|
||||
"pretty-quick": "1.6.0",
|
||||
"raw-loader": "0.5.1",
|
||||
"request": "2.88.0",
|
||||
"sinon": "6.0.1",
|
||||
|
|
22
src/cli.js
22
src/cli.js
|
@ -30,13 +30,17 @@ export function getConfig({ useCLI = true, argv } = {}) {
|
|||
// See #1762 for a rationale.
|
||||
const cliArgv = argv ? yargs(argv) : yargs;
|
||||
|
||||
return cliArgv
|
||||
.usage(`Usage: ./$0 [options] addon-package-or-dir \n\n
|
||||
Add-ons Linter (JS Edition) v${version}`)
|
||||
.options(options)
|
||||
// Require one non-option.
|
||||
.demand(1)
|
||||
.help('help')
|
||||
.alias('h', 'help')
|
||||
.wrap(terminalWidth());
|
||||
return (
|
||||
cliArgv
|
||||
.usage(
|
||||
`Usage: ./$0 [options] addon-package-or-dir \n\n
|
||||
Add-ons Linter (JS Edition) v${version}`
|
||||
)
|
||||
.options(options)
|
||||
// Require one non-option.
|
||||
.demand(1)
|
||||
.help('help')
|
||||
.alias('h', 'help')
|
||||
.wrap(terminalWidth())
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import * as constants from 'const';
|
|||
// "I have a display case ready and waiting for our newest acquisitions!"
|
||||
// --Taneleer Tivan
|
||||
|
||||
|
||||
export default class Collector {
|
||||
constructor(config = {}) {
|
||||
this.config = config;
|
||||
|
|
40
src/const.js
40
src/const.js
|
@ -15,16 +15,19 @@ export const EXTERNAL_RULE_MAPPING = {
|
|||
'no-unsafe-innerhtml/no-unsafe-innerhtml': ESLINT_WARNING,
|
||||
};
|
||||
|
||||
export const ESLINT_RULE_MAPPING = Object.assign({
|
||||
'deprecated-entities': ESLINT_WARNING,
|
||||
'event-listener-fourth': ESLINT_WARNING,
|
||||
'global-require-arg': ESLINT_WARNING,
|
||||
'opendialog-nonlit-uri': ESLINT_WARNING,
|
||||
'opendialog-remote-uri': ESLINT_WARNING,
|
||||
'webextension-api': ESLINT_WARNING,
|
||||
'webextension-unsupported-api': ESLINT_WARNING,
|
||||
'content-scripts-file-absent': ESLINT_ERROR,
|
||||
}, EXTERNAL_RULE_MAPPING);
|
||||
export const ESLINT_RULE_MAPPING = Object.assign(
|
||||
{
|
||||
'deprecated-entities': ESLINT_WARNING,
|
||||
'event-listener-fourth': ESLINT_WARNING,
|
||||
'global-require-arg': ESLINT_WARNING,
|
||||
'opendialog-nonlit-uri': ESLINT_WARNING,
|
||||
'opendialog-remote-uri': ESLINT_WARNING,
|
||||
'webextension-api': ESLINT_WARNING,
|
||||
'webextension-unsupported-api': ESLINT_WARNING,
|
||||
'content-scripts-file-absent': ESLINT_ERROR,
|
||||
},
|
||||
EXTERNAL_RULE_MAPPING
|
||||
);
|
||||
|
||||
export const VALIDATION_ERROR = 'error';
|
||||
export const VALIDATION_NOTICE = 'notice';
|
||||
|
@ -143,12 +146,14 @@ export const TEMPORARY_APIS = [
|
|||
// All valid CSP keywords that are options to keys like `default-src` and
|
||||
// `script-src`. Used in manifest.json parser for validation.
|
||||
// See https://mzl.la/2vwqbGU for more details and allowed options.
|
||||
export const CSP_KEYWORD_RE = new RegExp([
|
||||
'(self|none|unsafe-inline|strict-dynamic|unsafe-hashed-attributes)',
|
||||
// Only match these keywords, anything else is forbidden
|
||||
'(?!.)',
|
||||
'|(sha(256|384|512)-|nonce-)',
|
||||
].join(''));
|
||||
export const CSP_KEYWORD_RE = new RegExp(
|
||||
[
|
||||
'(self|none|unsafe-inline|strict-dynamic|unsafe-hashed-attributes)',
|
||||
// Only match these keywords, anything else is forbidden
|
||||
'(?!.)',
|
||||
'|(sha(256|384|512)-|nonce-)',
|
||||
].join('')
|
||||
);
|
||||
|
||||
// All badwords grouped by language as pre-compild regular expression.
|
||||
// Note: \b matches a very limited set of 'word boundaries' which might
|
||||
|
@ -167,4 +172,5 @@ export const LOCALES_DIRECTORY = '_locales';
|
|||
export const MESSAGE_PLACEHOLDER_REGEXP = '\\$([a-zA-Z0-9_@]+)\\$';
|
||||
|
||||
// yauzl should trow error with this message in case of corrupt zip file
|
||||
export const ZIP_LIB_CORRUPT_FILE_ERROR = 'end of central directory record signature not found';
|
||||
export const ZIP_LIB_CORRUPT_FILE_ERROR =
|
||||
'end of central directory record signature not found';
|
||||
|
|
|
@ -2,7 +2,6 @@ import { oneLine } from 'common-tags';
|
|||
|
||||
import { FLAGGED_FILE_MAGIC_NUMBERS_LENGTH, MAX_FILE_SIZE_MB } from 'const';
|
||||
|
||||
|
||||
/*
|
||||
* Base class for io operations for both an Xpi or
|
||||
* a directory
|
||||
|
|
|
@ -7,7 +7,6 @@ import log from 'logger';
|
|||
|
||||
import { Xpi } from './xpi';
|
||||
|
||||
|
||||
/*
|
||||
* A CRX file is just a ZIP file (eg an XPI) with some extra header
|
||||
* information. So we handle opening the file with a CRX parser, then treat
|
||||
|
|
|
@ -9,7 +9,6 @@ import { IOBase } from 'io/base';
|
|||
import { walkPromise } from 'io/utils';
|
||||
import log from 'logger';
|
||||
|
||||
|
||||
export class Directory extends IOBase {
|
||||
async getFiles(_walkPromise = walkPromise) {
|
||||
// If we have already processed this directory and have data
|
||||
|
@ -20,10 +19,9 @@ export class Directory extends IOBase {
|
|||
return this.files;
|
||||
}
|
||||
|
||||
const files = await _walkPromise(
|
||||
this.path, {
|
||||
shouldIncludePath: (...args) => this.shouldScanFile(...args),
|
||||
});
|
||||
const files = await _walkPromise(this.path, {
|
||||
shouldIncludePath: (...args) => this.shouldScanFile(...args),
|
||||
});
|
||||
|
||||
this.files = files;
|
||||
this.entries = Object.keys(files);
|
||||
|
@ -44,8 +42,10 @@ export class Directory extends IOBase {
|
|||
|
||||
// This is belt and braces. Should never happen that a file was in
|
||||
// the files object and yet doesn't meet these requirements.
|
||||
if (!filePath.startsWith(absoluteDirPath) ||
|
||||
relativeFilePath.startsWith('/')) {
|
||||
if (
|
||||
!filePath.startsWith(absoluteDirPath) ||
|
||||
relativeFilePath.startsWith('/')
|
||||
) {
|
||||
throw new Error(`Path argument must be relative to ${this.path}`);
|
||||
}
|
||||
|
||||
|
@ -95,14 +95,11 @@ export class Directory extends IOBase {
|
|||
// bytes if you are doing a binary check.
|
||||
encoding: null,
|
||||
autoClose: true,
|
||||
})
|
||||
.pipe(
|
||||
firstChunkStream({ chunkLength },
|
||||
(_, enc) => {
|
||||
resolve(enc);
|
||||
}
|
||||
)
|
||||
);
|
||||
}).pipe(
|
||||
firstChunkStream({ chunkLength }, (_, enc) => {
|
||||
resolve(enc);
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,10 +30,12 @@ export function walkPromise(curPath, { shouldIncludePath = () => true } = {}) {
|
|||
// Map the list of files and make a list of readdir
|
||||
// promises to pass to Promise.all so we can recursively
|
||||
// get the data on all the files in the directory.
|
||||
await Promise.all(files.map(async (fileName) => {
|
||||
await walk(path.join(_curPath, fileName));
|
||||
}));
|
||||
await Promise.all(
|
||||
files.map(async (fileName) => {
|
||||
await walk(path.join(_curPath, fileName));
|
||||
})
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}(curPath));
|
||||
})(curPath);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import { oneLine } from 'common-tags';
|
|||
import { IOBase } from 'io/base';
|
||||
import log from 'logger';
|
||||
|
||||
|
||||
/*
|
||||
* Simple Promise wrapper for the Yauzl unzipping lib to unpack add-on .xpis.
|
||||
* Note: We're using the autoclose feature of yauzl as a result every operation
|
||||
|
@ -43,8 +42,10 @@ export class Xpi extends IOBase {
|
|||
}
|
||||
if (this.entries.includes(entry.fileName)) {
|
||||
log.info('Found duplicate file entry: "%s" in package', entry.fileName);
|
||||
reject(new Error(oneLine`DuplicateZipEntry: Entry
|
||||
"${entry.fileName}" has already been seen`));
|
||||
reject(
|
||||
new Error(oneLine`DuplicateZipEntry: Entry
|
||||
"${entry.fileName}" has already been seen`)
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.entries.push(entry.fileName);
|
||||
|
@ -144,11 +145,9 @@ export class Xpi extends IOBase {
|
|||
return;
|
||||
}
|
||||
readStream.pipe(
|
||||
firstChunkStream({ chunkLength },
|
||||
(_, enc) => {
|
||||
resolve(enc);
|
||||
}
|
||||
)
|
||||
firstChunkStream({ chunkLength }, (_, enc) => {
|
||||
resolve(enc);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -321,5 +321,4 @@ export const BANNED_LIBRARIES = [
|
|||
'jquery.2.2.4.jquery.min.js',
|
||||
];
|
||||
|
||||
export const UNADVISED_LIBRARIES = [
|
||||
];
|
||||
export const UNADVISED_LIBRARIES = [];
|
||||
|
|
183
src/linter.js
183
src/linter.js
|
@ -29,14 +29,12 @@ import LangpackScanner from 'scanners/langpack';
|
|||
import { Crx, Directory, Xpi } from 'io';
|
||||
import { MINER_BLOCKLIST } from 'miner_blocklist';
|
||||
|
||||
|
||||
export default class Linter {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
([this.packagePath] = config._);
|
||||
[this.packagePath] = config._;
|
||||
this.io = null;
|
||||
this.chalk = new chalk.constructor(
|
||||
{ enabled: !this.config.boring });
|
||||
this.chalk = new chalk.constructor({ enabled: !this.config.boring });
|
||||
this.collector = new Collector(config);
|
||||
this.addonMetadata = null;
|
||||
this.shouldScanFile = this.shouldScanFile.bind(this);
|
||||
|
@ -49,8 +47,9 @@ export default class Linter {
|
|||
// convert into an array if needed and filter out any undefined
|
||||
// or empty strings.
|
||||
if (this._config.scanFile) {
|
||||
let scanFile = Array.isArray(this._config.scanFile) ?
|
||||
this._config.scanFile : [this._config.scanFile];
|
||||
let scanFile = Array.isArray(this._config.scanFile)
|
||||
? this._config.scanFile
|
||||
: [this._config.scanFile];
|
||||
scanFile = scanFile.filter((el) => el && el.length > 0);
|
||||
|
||||
this._config.scanFile = scanFile;
|
||||
|
@ -79,8 +78,7 @@ export default class Linter {
|
|||
if (err.message.includes('DuplicateZipEntry')) {
|
||||
this.collector.addError(messages.DUPLICATE_XPI_ENTRY);
|
||||
this.print(_console);
|
||||
} else if (err.message.includes(
|
||||
constants.ZIP_LIB_CORRUPT_FILE_ERROR)) {
|
||||
} else if (err.message.includes(constants.ZIP_LIB_CORRUPT_FILE_ERROR)) {
|
||||
this.collector.addError(messages.BAD_ZIPFILE);
|
||||
this.print(_console);
|
||||
} else if (this.config.stack === true) {
|
||||
|
@ -101,7 +99,11 @@ export default class Linter {
|
|||
}
|
||||
}
|
||||
|
||||
toJSON({ input = this.output, pretty = this.config.pretty, _JSON = JSON } = {}) {
|
||||
toJSON({
|
||||
input = this.output,
|
||||
pretty = this.config.pretty,
|
||||
_JSON = JSON,
|
||||
} = {}) {
|
||||
const args = [input];
|
||||
if (pretty === true) {
|
||||
args.push(null);
|
||||
|
@ -116,10 +118,12 @@ export default class Linter {
|
|||
|
||||
out.push(i18n._('Validation Summary:'));
|
||||
out.push('');
|
||||
out.push(columnify(this.output.summary, {
|
||||
showHeaders: false,
|
||||
minWidth: 15,
|
||||
}));
|
||||
out.push(
|
||||
columnify(this.output.summary, {
|
||||
showHeaders: false,
|
||||
minWidth: 15,
|
||||
})
|
||||
);
|
||||
out.push('');
|
||||
|
||||
constants.MESSAGE_TYPES.forEach((type) => {
|
||||
|
@ -198,12 +202,14 @@ export default class Linter {
|
|||
|
||||
out.push(`${messageType.toUpperCase()}:`);
|
||||
out.push('');
|
||||
out.push(columnify(this.output[messageType], {
|
||||
maxWidth: 35,
|
||||
columns: outputColumns,
|
||||
columnSplitter: ' ',
|
||||
config: outputConfig,
|
||||
}));
|
||||
out.push(
|
||||
columnify(this.output[messageType], {
|
||||
maxWidth: 35,
|
||||
columns: outputColumns,
|
||||
columnSplitter: ' ',
|
||||
config: outputConfig,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -246,20 +252,18 @@ export default class Linter {
|
|||
if (Object.prototype.hasOwnProperty.call(files, constants.MANIFEST_JSON)) {
|
||||
_log.info('Retrieving metadata from manifest.json');
|
||||
const json = await this.io.getFileAsString(constants.MANIFEST_JSON);
|
||||
const manifestParser = new ManifestJSONParser(
|
||||
json,
|
||||
this.collector,
|
||||
{
|
||||
selfHosted: this.config.selfHosted,
|
||||
io: this.io,
|
||||
},
|
||||
);
|
||||
const manifestParser = new ManifestJSONParser(json, this.collector, {
|
||||
selfHosted: this.config.selfHosted,
|
||||
io: this.io,
|
||||
});
|
||||
if (manifestParser.parsedJSON.icons) {
|
||||
await manifestParser.validateIcons();
|
||||
}
|
||||
this.addonMetadata = manifestParser.getMetadata();
|
||||
} else {
|
||||
_log.warn(`No ${constants.MANIFEST_JSON} was found in the package metadata`);
|
||||
_log.warn(
|
||||
`No ${constants.MANIFEST_JSON} was found in the package metadata`
|
||||
);
|
||||
this.collector.addNotice(messages.TYPE_NO_MANIFEST_JSON);
|
||||
this.addonMetadata = {};
|
||||
}
|
||||
|
@ -269,7 +273,8 @@ export default class Linter {
|
|||
|
||||
async checkFileExists(filepath, _lstatPromise = lstatPromise) {
|
||||
const invalidMessage = new Error(
|
||||
`Path "${filepath}" is not a file or directory or does not exist.`);
|
||||
`Path "${filepath}" is not a file or directory or does not exist.`
|
||||
);
|
||||
try {
|
||||
const stats = await _lstatPromise(filepath);
|
||||
if (stats.isFile() === true || stats.isDirectory() === true) {
|
||||
|
@ -292,10 +297,12 @@ export default class Linter {
|
|||
}
|
||||
|
||||
getScanner(filename) {
|
||||
if (filename.match(constants.HIDDEN_FILE_REGEX) ||
|
||||
filename.match(constants.FLAGGED_FILE_REGEX) ||
|
||||
constants.FLAGGED_FILE_EXTENSIONS.includes(path.extname(filename)) ||
|
||||
filename.match(constants.ALREADY_SIGNED_REGEX)) {
|
||||
if (
|
||||
filename.match(constants.HIDDEN_FILE_REGEX) ||
|
||||
filename.match(constants.FLAGGED_FILE_REGEX) ||
|
||||
constants.FLAGGED_FILE_EXTENSIONS.includes(path.extname(filename)) ||
|
||||
filename.match(constants.ALREADY_SIGNED_REGEX)
|
||||
) {
|
||||
return FilenameScanner;
|
||||
}
|
||||
|
||||
|
@ -321,14 +328,18 @@ export default class Linter {
|
|||
async scanFile(filename) {
|
||||
let scanResult = { linterMessages: [], scannedFiles: [] };
|
||||
const ScannerClass = this.getScanner(filename);
|
||||
const fileData = await this.io.getFile(filename, ScannerClass.fileResultType);
|
||||
const fileData = await this.io.getFile(
|
||||
filename,
|
||||
ScannerClass.fileResultType
|
||||
);
|
||||
|
||||
// First: check that this file is under our 2MB parsing limit. Otherwise
|
||||
// it will be very slow and may crash the lint with an out-of-memory
|
||||
// error.
|
||||
const fileSize = typeof this.io.files[filename].size !== 'undefined' ?
|
||||
this.io.files[filename].size :
|
||||
this.io.files[filename].uncompressedSize;
|
||||
const fileSize =
|
||||
typeof this.io.files[filename].size !== 'undefined'
|
||||
? this.io.files[filename].size
|
||||
: this.io.files[filename].uncompressedSize;
|
||||
const maxSize = 1024 * 1024 * constants.MAX_FILE_SIZE_TO_PARSE_MB;
|
||||
|
||||
if (ScannerClass !== BinaryScanner && fileSize >= maxSize) {
|
||||
|
@ -385,7 +396,10 @@ export default class Linter {
|
|||
}
|
||||
|
||||
async extractMetadata({
|
||||
_Crx = Crx, _console = console, _Directory = Directory, _Xpi = Xpi,
|
||||
_Crx = Crx,
|
||||
_console = console,
|
||||
_Directory = Directory,
|
||||
_Xpi = Xpi,
|
||||
} = {}) {
|
||||
await checkMinNodeVersion();
|
||||
|
||||
|
@ -437,9 +451,7 @@ export default class Linter {
|
|||
}
|
||||
|
||||
if (this.config.scanFile) {
|
||||
const manifestFileNames = [
|
||||
'manifest.json', 'package.json',
|
||||
];
|
||||
const manifestFileNames = ['manifest.json', 'package.json'];
|
||||
|
||||
// Always scan sub directories and the manifest files,
|
||||
// or the linter will not be able to detect the addon type.
|
||||
|
@ -459,15 +471,20 @@ export default class Linter {
|
|||
await this.extractMetadata(deps);
|
||||
const files = await this.io.getFiles();
|
||||
|
||||
if (this.config.scanFile &&
|
||||
!this.config.scanFile.some((f) => Object.keys(files).includes(f))) {
|
||||
if (
|
||||
this.config.scanFile &&
|
||||
!this.config.scanFile.some((f) => Object.keys(files).includes(f))
|
||||
) {
|
||||
const _files = this.config.scanFile.join(', ');
|
||||
throw new Error(`Selected file(s) not found: ${_files}`);
|
||||
}
|
||||
|
||||
// Known libraries do not need to be scanned
|
||||
const filesWithoutJSLibraries = Object.keys(files).filter((file) => {
|
||||
return !Object.prototype.hasOwnProperty.call(this.addonMetadata.jsLibs, file);
|
||||
return !Object.prototype.hasOwnProperty.call(
|
||||
this.addonMetadata.jsLibs,
|
||||
file
|
||||
);
|
||||
});
|
||||
|
||||
await this.scanFiles(filesWithoutJSLibraries);
|
||||
|
@ -547,13 +564,17 @@ export default class Linter {
|
|||
|
||||
const files = await this.io.getFiles();
|
||||
Object.keys(files).forEach((filename) => {
|
||||
if (typeof files[filename].size === 'undefined' &&
|
||||
typeof files[filename].uncompressedSize === 'undefined') {
|
||||
if (
|
||||
typeof files[filename].size === 'undefined' &&
|
||||
typeof files[filename].uncompressedSize === 'undefined'
|
||||
) {
|
||||
throw new Error(`No size available for ${filename}`);
|
||||
}
|
||||
|
||||
if (files[filename].size === 0 ||
|
||||
files[filename].uncompressedSize === 0) {
|
||||
if (
|
||||
files[filename].size === 0 ||
|
||||
files[filename].uncompressedSize === 0
|
||||
) {
|
||||
emptyFiles.push(filename);
|
||||
}
|
||||
});
|
||||
|
@ -568,21 +589,23 @@ export default class Linter {
|
|||
const jsLibs = {};
|
||||
const files = await this.io.getFilesByExt('.js');
|
||||
|
||||
await Promise.all(files.map(async (filename) => {
|
||||
const file = await this.io.getFile(filename);
|
||||
const hashResult = dispensary.match(file);
|
||||
await Promise.all(
|
||||
files.map(async (filename) => {
|
||||
const file = await this.io.getFile(filename);
|
||||
const hashResult = dispensary.match(file);
|
||||
|
||||
if (hashResult !== false) {
|
||||
log.debug(`${hashResult} detected in ${filename}`);
|
||||
jsLibs[filename] = hashResult;
|
||||
if (hashResult !== false) {
|
||||
log.debug(`${hashResult} detected in ${filename}`);
|
||||
jsLibs[filename] = hashResult;
|
||||
|
||||
this.collector.addNotice(
|
||||
Object.assign({}, messages.KNOWN_LIBRARY, {
|
||||
file: filename,
|
||||
})
|
||||
);
|
||||
}
|
||||
}));
|
||||
this.collector.addNotice(
|
||||
Object.assign({}, messages.KNOWN_LIBRARY, {
|
||||
file: filename,
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
addonMetadata.jsLibs = jsLibs;
|
||||
|
@ -594,16 +617,18 @@ export default class Linter {
|
|||
|
||||
const files = await this.io.getFilesByExt('.js');
|
||||
|
||||
await Promise.all(files.map(async (filename) => {
|
||||
if (filename in addonMetadata.jsLibs) {
|
||||
return;
|
||||
}
|
||||
const fileData = await this.io.getFile(filename);
|
||||
if (couldBeMinifiedCode(fileData)) {
|
||||
log.debug(`Minified code detected in ${filename}`);
|
||||
unknownMinifiedFiles.push(filename);
|
||||
}
|
||||
}));
|
||||
await Promise.all(
|
||||
files.map(async (filename) => {
|
||||
if (filename in addonMetadata.jsLibs) {
|
||||
return;
|
||||
}
|
||||
const fileData = await this.io.getFile(filename);
|
||||
if (couldBeMinifiedCode(fileData)) {
|
||||
log.debug(`Minified code detected in ${filename}`);
|
||||
unknownMinifiedFiles.push(filename);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
addonMetadata.unknownMinifiedFiles = unknownMinifiedFiles;
|
||||
|
@ -633,21 +658,24 @@ export default class Linter {
|
|||
this.collector.addWarning(
|
||||
Object.assign({}, messages.COINMINER_USAGE_DETECTED, {
|
||||
file: filename,
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const fileDataMatch = fileData.match(nameRegex);
|
||||
|
||||
if (fileDataMatch) {
|
||||
const { matchedLine, matchedColumn } = getLineAndColumnFromMatch(
|
||||
fileDataMatch);
|
||||
fileDataMatch
|
||||
);
|
||||
|
||||
this.collector.addWarning(
|
||||
Object.assign({}, messages.COINMINER_USAGE_DETECTED, {
|
||||
file: filename,
|
||||
column: matchedColumn,
|
||||
line: matchedLine,
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -655,7 +683,9 @@ export default class Linter {
|
|||
const match = fileData.match(codeRegex);
|
||||
|
||||
if (match) {
|
||||
const { matchedLine, matchedColumn } = getLineAndColumnFromMatch(match);
|
||||
const { matchedLine, matchedColumn } = getLineAndColumnFromMatch(
|
||||
match
|
||||
);
|
||||
|
||||
this.collector.addWarning(
|
||||
Object.assign({}, messages.COINMINER_USAGE_DETECTED, {
|
||||
|
@ -665,7 +695,8 @@ export default class Linter {
|
|||
// use dataPath for our actual match to avoid any obvious
|
||||
// duplicates
|
||||
dataPath: match[0],
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import pino from 'pino';
|
||||
|
||||
|
||||
export function createLogger(_process = process) {
|
||||
const level = _process.env.LOG_LEVEL || 'fatal';
|
||||
return pino({
|
||||
name: 'AddonLinterJS',
|
||||
level,
|
||||
}, process.stdout);
|
||||
return pino(
|
||||
{
|
||||
name: 'AddonLinterJS',
|
||||
level,
|
||||
},
|
||||
process.stdout
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default createLogger();
|
||||
|
|
|
@ -2,13 +2,13 @@ import { getConfig } from 'cli';
|
|||
import Linter from 'linter';
|
||||
import log from 'logger';
|
||||
|
||||
|
||||
export function isRunFromCLI(_module = module) {
|
||||
return require.main === _module;
|
||||
}
|
||||
|
||||
export function createInstance({
|
||||
config = getConfig({ useCLI: isRunFromCLI() }).argv, runAsBinary = false,
|
||||
config = getConfig({ useCLI: isRunFromCLI() }).argv,
|
||||
runAsBinary = false,
|
||||
} = {}) {
|
||||
log.level = config.logLevel;
|
||||
log.info('Creating new linter instance', { config });
|
||||
|
|
|
@ -2,7 +2,6 @@ import { oneLine } from 'common-tags';
|
|||
|
||||
import { MESSAGE_TYPES } from 'const';
|
||||
|
||||
|
||||
// These are the props we expect to pull out of
|
||||
// the data object passed to the Message constructor.
|
||||
export const props = [
|
||||
|
@ -15,11 +14,7 @@ export const props = [
|
|||
'dataPath',
|
||||
];
|
||||
|
||||
export const requiredProps = [
|
||||
'code',
|
||||
'message',
|
||||
'description',
|
||||
];
|
||||
export const requiredProps = ['code', 'message', 'description'];
|
||||
|
||||
export default class Message {
|
||||
constructor(type, data = {}) {
|
||||
|
@ -57,8 +52,11 @@ export default class Message {
|
|||
}
|
||||
|
||||
matches(other) {
|
||||
return this.type === other.type && props.every((prop) => {
|
||||
return this[prop] === other[prop];
|
||||
});
|
||||
return (
|
||||
this.type === other.type &&
|
||||
props.every((prop) => {
|
||||
return this[prop] === other[prop];
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ import { oneLine } from 'common-tags';
|
|||
|
||||
import { i18n } from 'utils';
|
||||
|
||||
|
||||
export const CSS_SYNTAX_ERROR = {
|
||||
code: 'CSS_SYNTAX_ERROR',
|
||||
// This will be overriden by the reason passed from the error.
|
||||
|
|
|
@ -2,7 +2,6 @@ import { oneLine } from 'common-tags';
|
|||
|
||||
import { i18n } from 'utils';
|
||||
|
||||
|
||||
export const INLINE_SCRIPT = {
|
||||
code: 'INLINE_SCRIPT',
|
||||
message: i18n._('Inline scripts blocked by default'),
|
||||
|
@ -10,7 +9,6 @@ export const INLINE_SCRIPT = {
|
|||
from running (https://mzl.la/2pn32nd).`),
|
||||
};
|
||||
|
||||
|
||||
export const REMOTE_SCRIPT = {
|
||||
code: 'REMOTE_SCRIPT',
|
||||
message: i18n._('Remote scripts are not allowed as per the Add-on Policies.'),
|
||||
|
|
|
@ -136,13 +136,13 @@ function deprecatedAPI(api) {
|
|||
export const APP_GETDETAILS = deprecatedAPI('app.getDetails');
|
||||
export const EXT_ONREQUEST = deprecatedAPI('extension.onRequest');
|
||||
export const EXT_ONREQUESTEXTERNAL = deprecatedAPI(
|
||||
'extension.onRequestExternal');
|
||||
'extension.onRequestExternal'
|
||||
);
|
||||
export const EXT_SENDREQUEST = deprecatedAPI('extension.sendRequest');
|
||||
export const TABS_GETALLINWINDOW = deprecatedAPI('tabs.getAllInWindow');
|
||||
export const TABS_GETSELECTED = deprecatedAPI('tabs.getSelected');
|
||||
export const TABS_ONACTIVECHANGED = deprecatedAPI('tabs.onActiveChanged');
|
||||
export const TABS_ONSELECTIONCHANGED = deprecatedAPI(
|
||||
'tabs.onSelectionChanged');
|
||||
export const TABS_ONSELECTIONCHANGED = deprecatedAPI('tabs.onSelectionChanged');
|
||||
export const TABS_SENDREQUEST = deprecatedAPI('tabs.sendRequest');
|
||||
|
||||
function temporaryAPI(api) {
|
||||
|
|
|
@ -3,7 +3,6 @@ import { oneLine } from 'common-tags';
|
|||
import { MAX_FILE_SIZE_TO_PARSE_MB } from 'const';
|
||||
import { i18n } from 'utils';
|
||||
|
||||
|
||||
export const DUPLICATE_XPI_ENTRY = {
|
||||
code: 'DUPLICATE_XPI_ENTRY',
|
||||
message: i18n._('Package contains duplicate entries'),
|
||||
|
|
|
@ -5,7 +5,9 @@ import { i18n } from 'utils';
|
|||
export const NO_MESSAGE = {
|
||||
code: 'NO_MESSAGE',
|
||||
message: i18n._('Translation string is missing the message property'),
|
||||
description: i18n._('No "message" message property is set for a string (https://mzl.la/2DSBTjA).'),
|
||||
description: i18n._(
|
||||
'No "message" message property is set for a string (https://mzl.la/2DSBTjA).'
|
||||
),
|
||||
};
|
||||
|
||||
export const PREDEFINED_MESSAGE_NAME = {
|
||||
|
|
|
@ -3,18 +3,21 @@ import { oneLine } from 'common-tags';
|
|||
import { i18n } from 'utils';
|
||||
import { MANIFEST_JSON } from 'const';
|
||||
|
||||
|
||||
export const MANIFEST_FIELD_REQUIRED = {
|
||||
code: 'MANIFEST_FIELD_REQUIRED',
|
||||
message: i18n._('The field is required.'),
|
||||
description: i18n._('See https://mzl.la/1ZOhoEN (MDN Docs) for more information.'),
|
||||
description: i18n._(
|
||||
'See https://mzl.la/1ZOhoEN (MDN Docs) for more information.'
|
||||
),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
|
||||
export const MANIFEST_FIELD_INVALID = {
|
||||
code: 'MANIFEST_FIELD_INVALID',
|
||||
message: i18n._('The field is invalid.'),
|
||||
description: i18n._('See https://mzl.la/1ZOhoEN (MDN Docs) for more information.'),
|
||||
description: i18n._(
|
||||
'See https://mzl.la/1ZOhoEN (MDN Docs) for more information.'
|
||||
),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
|
||||
|
@ -29,14 +32,20 @@ export const MANIFEST_BAD_PERMISSION = {
|
|||
export const MANIFEST_PERMISSIONS = {
|
||||
code: 'MANIFEST_PERMISSIONS',
|
||||
message: i18n._('Unknown permission.'),
|
||||
description: i18n._('See https://mzl.la/1R1n1t0 (MDN Docs) for more information.'),
|
||||
description: i18n._(
|
||||
'See https://mzl.la/1R1n1t0 (MDN Docs) for more information.'
|
||||
),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
|
||||
export const MANIFEST_VERSION_INVALID = {
|
||||
code: 'MANIFEST_VERSION_INVALID',
|
||||
message: i18n._('"manifest_version" in the manifest.json is not a valid value'),
|
||||
description: i18n._('See https://mzl.la/20PenXl (MDN Docs) for more information.'),
|
||||
message: i18n._(
|
||||
'"manifest_version" in the manifest.json is not a valid value'
|
||||
),
|
||||
description: i18n._(
|
||||
'See https://mzl.la/20PenXl (MDN Docs) for more information.'
|
||||
),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
|
||||
|
@ -46,7 +55,9 @@ export const MANIFEST_CSP = {
|
|||
code: 'MANIFEST_CSP',
|
||||
message: i18n._(oneLine`
|
||||
"content_security_policy" allows remote code execution in manifest.json`),
|
||||
description: i18n._('A custom content_security_policy needs additional review.'),
|
||||
description: i18n._(
|
||||
'A custom content_security_policy needs additional review.'
|
||||
),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
|
||||
|
@ -63,21 +74,27 @@ export const MANIFEST_CSP_UNSAFE_EVAL = {
|
|||
export const PROP_NAME_INVALID = {
|
||||
code: 'PROP_NAME_INVALID',
|
||||
message: i18n._('The "name" property must be a string.'),
|
||||
description: i18n._('See http://mzl.la/1STmr48 (MDN Docs) for more information.'),
|
||||
description: i18n._(
|
||||
'See http://mzl.la/1STmr48 (MDN Docs) for more information.'
|
||||
),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
|
||||
export const PROP_VERSION_INVALID = {
|
||||
code: 'PROP_VERSION_INVALID',
|
||||
message: i18n._('The "version" property must be a string.'),
|
||||
description: i18n._('See http://mzl.la/1kXIADa (MDN Docs) for more information.'),
|
||||
description: i18n._(
|
||||
'See http://mzl.la/1kXIADa (MDN Docs) for more information.'
|
||||
),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
|
||||
export const PROP_VERSION_TOOLKIT_ONLY = {
|
||||
code: 'PROP_VERSION_TOOLKIT_ONLY',
|
||||
message: i18n._('The "version" property uses a Firefox-specific format.'),
|
||||
description: i18n._('See http://mzl.la/1kXIADa (MDN Docs) for more information.'),
|
||||
description: i18n._(
|
||||
'See http://mzl.la/1kXIADa (MDN Docs) for more information.'
|
||||
),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
|
||||
|
@ -120,48 +137,62 @@ export function manifestIconMissing(path) {
|
|||
return {
|
||||
code: MANIFEST_ICON_NOT_FOUND,
|
||||
message: i18n._(
|
||||
'An icon defined in the manifest could not be found in the package.'),
|
||||
'An icon defined in the manifest could not be found in the package.'
|
||||
),
|
||||
description: i18n.sprintf(
|
||||
i18n._('Icon could not be found at "%(path)s".'),
|
||||
{ path }),
|
||||
{ path }
|
||||
),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
}
|
||||
|
||||
export const MANIFEST_BACKGROUND_FILE_NOT_FOUND = 'MANIFEST_BACKGROUND_FILE_NOT_FOUND';
|
||||
export const MANIFEST_BACKGROUND_FILE_NOT_FOUND =
|
||||
'MANIFEST_BACKGROUND_FILE_NOT_FOUND';
|
||||
export function manifestBackgroundMissing(path, type) {
|
||||
return {
|
||||
code: MANIFEST_BACKGROUND_FILE_NOT_FOUND,
|
||||
legacyCode: null,
|
||||
message: type === 'script' ?
|
||||
i18n._('A background script defined in the manifest could not be found.') :
|
||||
i18n._('A background page defined in the manifest could not be found.'),
|
||||
description:
|
||||
i18n.sprintf(
|
||||
type === 'script' ?
|
||||
i18n._('Background script could not be found at "%(path)s".') :
|
||||
i18n._('Background page could not be found at "%(path)s".'),
|
||||
{ path }
|
||||
),
|
||||
message:
|
||||
type === 'script'
|
||||
? i18n._(
|
||||
'A background script defined in the manifest could not be found.'
|
||||
)
|
||||
: i18n._(
|
||||
'A background page defined in the manifest could not be found.'
|
||||
),
|
||||
description: i18n.sprintf(
|
||||
type === 'script'
|
||||
? i18n._('Background script could not be found at "%(path)s".')
|
||||
: i18n._('Background page could not be found at "%(path)s".'),
|
||||
{ path }
|
||||
),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
}
|
||||
|
||||
export const MANIFEST_CONTENT_SCRIPT_FILE_NOT_FOUND = 'MANIFEST_CONTENT_SCRIPT_FILE_NOT_FOUND';
|
||||
export const MANIFEST_CONTENT_SCRIPT_FILE_NOT_FOUND =
|
||||
'MANIFEST_CONTENT_SCRIPT_FILE_NOT_FOUND';
|
||||
export function manifestContentScriptFileMissing(path, type) {
|
||||
return {
|
||||
code: MANIFEST_CONTENT_SCRIPT_FILE_NOT_FOUND,
|
||||
legacyCode: null,
|
||||
message: type === 'script' ?
|
||||
i18n._('A content script defined in the manifest could not be found.') :
|
||||
i18n._('A content script css file defined in the manifest could not be found.'),
|
||||
description:
|
||||
i18n.sprintf(
|
||||
type === 'script' ?
|
||||
i18n._('Content script defined in the manifest could not be found at "%(path)s".') :
|
||||
i18n._('Content script css file defined in the manifest could not be found at "%(path)s".'),
|
||||
{ path }
|
||||
),
|
||||
message:
|
||||
type === 'script'
|
||||
? i18n._('A content script defined in the manifest could not be found.')
|
||||
: i18n._(
|
||||
'A content script css file defined in the manifest could not be found.'
|
||||
),
|
||||
description: i18n.sprintf(
|
||||
type === 'script'
|
||||
? i18n._(
|
||||
'Content script defined in the manifest could not be found at "%(path)s".'
|
||||
)
|
||||
: i18n._(
|
||||
'Content script css file defined in the manifest could not be found at "%(path)s".'
|
||||
),
|
||||
{ path }
|
||||
),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
}
|
||||
|
@ -171,12 +202,15 @@ export function manifestDictionaryFileMissing(path) {
|
|||
return {
|
||||
code: MANIFEST_DICT_NOT_FOUND,
|
||||
legacyCode: null,
|
||||
message: i18n._('A dictionary file defined in the manifest could not be found.'),
|
||||
description:
|
||||
i18n.sprintf(
|
||||
i18n._('Dictionary file defined in the manifest could not be found at "%(path)s".'),
|
||||
{ path }
|
||||
message: i18n._(
|
||||
'A dictionary file defined in the manifest could not be found.'
|
||||
),
|
||||
description: i18n.sprintf(
|
||||
i18n._(
|
||||
'Dictionary file defined in the manifest could not be found at "%(path)s".'
|
||||
),
|
||||
{ path }
|
||||
),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
}
|
||||
|
@ -185,17 +219,21 @@ export const MANIFEST_MULTIPLE_DICTS = {
|
|||
code: 'MANIFEST_MULTIPLE_DICTS',
|
||||
legacyCode: null,
|
||||
message: i18n._('The manifest contains multiple dictionaries.'),
|
||||
description:
|
||||
i18n._('Multiple dictionaries were defined in the manifest, which is unsupported.'),
|
||||
description: i18n._(
|
||||
'Multiple dictionaries were defined in the manifest, which is unsupported.'
|
||||
),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
|
||||
export const MANIFEST_EMPTY_DICTS = {
|
||||
code: 'MANIFEST_EMPTY_DICTS',
|
||||
legacyCode: null,
|
||||
message: i18n._('The manifest contains a dictionaries object, but it is empty.'),
|
||||
description:
|
||||
i18n._('A dictionaries object was defined in the manifest, but it was empty.'),
|
||||
message: i18n._(
|
||||
'The manifest contains a dictionaries object, but it is empty.'
|
||||
),
|
||||
description: i18n._(
|
||||
'A dictionaries object was defined in the manifest, but it was empty.'
|
||||
),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
|
||||
|
@ -213,7 +251,9 @@ export function iconIsNotSquare(path) {
|
|||
return {
|
||||
code: ICON_NOT_SQUARE,
|
||||
message: i18n._('Icons must be square.'),
|
||||
description: i18n.sprintf(i18n._('Icon at "%(path)s" must be square.'), { path }),
|
||||
description: i18n.sprintf(i18n._('Icon at "%(path)s" must be square.'), {
|
||||
path,
|
||||
}),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
}
|
||||
|
@ -223,9 +263,12 @@ export function iconSizeInvalid({ path, expected, actual }) {
|
|||
return {
|
||||
code: ICON_SIZE_INVALID,
|
||||
message: i18n._('The size of the icon does not match the manifest.'),
|
||||
description: i18n.sprintf(i18n._(oneLine`
|
||||
description: i18n.sprintf(
|
||||
i18n._(oneLine`
|
||||
Expected icon at "%(path)s" to be %(expected)d pixels wide but was %(actual)d.
|
||||
`), { path, expected, actual }),
|
||||
`),
|
||||
{ path, expected, actual }
|
||||
),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
}
|
||||
|
@ -237,7 +280,8 @@ export function corruptIconFile({ path }) {
|
|||
message: i18n._('Corrupt image file'),
|
||||
description: i18n.sprintf(
|
||||
i18n._('Expected icon file at "%(path)s" is corrupted'),
|
||||
{ path }),
|
||||
{ path }
|
||||
),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
}
|
||||
|
@ -266,7 +310,9 @@ export const NO_DEFAULT_LOCALE = {
|
|||
export const WRONG_ICON_EXTENSION = {
|
||||
code: 'WRONG_ICON_EXTENSION',
|
||||
message: i18n._('Unsupported image extension'),
|
||||
description: i18n._('Icons should be one of JPG/JPEG, WebP, GIF, PNG or SVG.'),
|
||||
description: i18n._(
|
||||
'Icons should be one of JPG/JPEG, WebP, GIF, PNG or SVG.'
|
||||
),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
|
||||
|
@ -275,7 +321,10 @@ export function noMessagesFileInLocales(path) {
|
|||
return {
|
||||
code: NO_MESSAGES_FILE_IN_LOCALES,
|
||||
message: i18n._('Empty language directory'),
|
||||
description: i18n.sprintf(i18n._('messages.json file missing in "%(path)s"'), { path }),
|
||||
description: i18n.sprintf(
|
||||
i18n._('messages.json file missing in "%(path)s"'),
|
||||
{ path }
|
||||
),
|
||||
file: MANIFEST_JSON,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,8 +6,5 @@ export const MINER_BLOCKLIST = {
|
|||
/\bcryptonight_hash\b/,
|
||||
/CryptonightWASMWrapper/,
|
||||
],
|
||||
filenames: [
|
||||
/coinhive(\.min)?\.js/,
|
||||
/cryptonight(\.min)\.js/,
|
||||
],
|
||||
filenames: [/coinhive(\.min)?\.js/, /cryptonight(\.min)\.js/],
|
||||
};
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
const ENTITY_RE = /<!ENTITY\s+([\w.]*)\s+("[^"]*"|'[^']*')\s*>/;
|
||||
|
||||
|
||||
export default class DoctypeParser {
|
||||
/*
|
||||
* Minimalistic parser for DTD files.
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
|
||||
import * as messages from 'messages';
|
||||
|
||||
|
||||
export default class FluentParser {
|
||||
/*
|
||||
* Parse FTL files (https://projectfluent.io)
|
||||
|
@ -34,8 +33,12 @@ export default class FluentParser {
|
|||
|
||||
// There is always just one annotation for a junk entry
|
||||
const annotation = entry.annotations[0];
|
||||
const matchedLine = lineOffset(this._sourceString, annotation.span.end) + 1;
|
||||
const matchedColumn = columnOffset(this._sourceString, annotation.span.end);
|
||||
const matchedLine =
|
||||
lineOffset(this._sourceString, annotation.span.end) + 1;
|
||||
const matchedColumn = columnOffset(
|
||||
this._sourceString,
|
||||
annotation.span.end
|
||||
);
|
||||
|
||||
const errorData = {
|
||||
...messages.FLUENT_INVALID,
|
||||
|
|
|
@ -41,7 +41,9 @@ export default class JSONParser {
|
|||
// See:
|
||||
// http://stackoverflow.com/questions/23752156/are-all-json-objects-also-valid-javascript-objects/23753148#23753148
|
||||
// https://github.com/judofyr/timeless/issues/57#issuecomment-31872462
|
||||
const tokens = esprima.tokenize(manifestString, { comment: true }).slice(3);
|
||||
const tokens = esprima
|
||||
.tokenize(manifestString, { comment: true })
|
||||
.slice(3);
|
||||
this._jsonString = tokens.reduce((json, token) => {
|
||||
// Ignore line comments (`// comments`) and just return the existing
|
||||
// json we've built.
|
||||
|
|
|
@ -7,9 +7,11 @@ import { validateLocaleMessages } from 'schema/validator';
|
|||
import log from 'logger';
|
||||
|
||||
export default class LocaleMessagesJSONParser extends JSONParser {
|
||||
constructor(jsonString, collector, {
|
||||
filename = MESSAGES_JSON, RelaxedJSON = RJSON,
|
||||
} = {}) {
|
||||
constructor(
|
||||
jsonString,
|
||||
collector,
|
||||
{ filename = MESSAGES_JSON, RelaxedJSON = RJSON } = {}
|
||||
) {
|
||||
super(jsonString, collector, { filename });
|
||||
this.relaxedJSON = RelaxedJSON;
|
||||
}
|
||||
|
@ -45,7 +47,9 @@ export default class LocaleMessagesJSONParser extends JSONParser {
|
|||
baseObject = messages.NO_PLACEHOLDER_CONTENT;
|
||||
}
|
||||
} else if (error.keyword === 'additionalProperties') {
|
||||
if (error.schemaPath === '#/properties/placeholders/additionalProperties') {
|
||||
if (
|
||||
error.schemaPath === '#/properties/placeholders/additionalProperties'
|
||||
) {
|
||||
baseObject = messages.INVALID_PLACEHOLDER_NAME;
|
||||
}
|
||||
}
|
||||
|
@ -58,17 +62,22 @@ export default class LocaleMessagesJSONParser extends JSONParser {
|
|||
if (!Object.prototype.hasOwnProperty.call(messageObj, 'placeholders')) {
|
||||
return undefined;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(this.lowercasePlaceholders, message)) {
|
||||
this.lowercasePlaceholders[message] = Object.keys(messageObj.placeholders)
|
||||
.map((placeholder) => placeholder.toLowerCase());
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(this.lowercasePlaceholders, message)
|
||||
) {
|
||||
this.lowercasePlaceholders[message] = Object.keys(
|
||||
messageObj.placeholders
|
||||
).map((placeholder) => placeholder.toLowerCase());
|
||||
}
|
||||
return this.lowercasePlaceholders[message];
|
||||
}
|
||||
|
||||
hasPlaceholder(message, placeholder) {
|
||||
const messageObj = this.parsedJSON[message];
|
||||
return Object.prototype.hasOwnProperty.call(messageObj, 'placeholders') &&
|
||||
this.getLowercasePlaceholders(message).includes(placeholder.toLowerCase());
|
||||
return (
|
||||
Object.prototype.hasOwnProperty.call(messageObj, 'placeholders') &&
|
||||
this.getLowercasePlaceholders(message).includes(placeholder.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
_validate() {
|
||||
|
@ -88,47 +97,70 @@ export default class LocaleMessagesJSONParser extends JSONParser {
|
|||
if (!visitedLowercaseMessages.includes(message.toLowerCase())) {
|
||||
visitedLowercaseMessages.push(message.toLowerCase());
|
||||
} else {
|
||||
this.collector.addError(Object.assign({}, messages.JSON_DUPLICATE_KEY, {
|
||||
file: this.filename,
|
||||
description: `Case-insensitive duplicate message name: ${message} found in JSON`,
|
||||
dataPath: `/${message}`,
|
||||
}));
|
||||
this.collector.addError(
|
||||
Object.assign({}, messages.JSON_DUPLICATE_KEY, {
|
||||
file: this.filename,
|
||||
description: `Case-insensitive duplicate message name: ${message} found in JSON`,
|
||||
dataPath: `/${message}`,
|
||||
})
|
||||
);
|
||||
this.isValid = false;
|
||||
}
|
||||
|
||||
if (message.startsWith('@@')) {
|
||||
this.collector.addWarning(Object.assign({
|
||||
file: this.filename,
|
||||
dataPath: `/${message}`,
|
||||
}, messages.PREDEFINED_MESSAGE_NAME));
|
||||
this.collector.addWarning(
|
||||
Object.assign(
|
||||
{
|
||||
file: this.filename,
|
||||
dataPath: `/${message}`,
|
||||
},
|
||||
messages.PREDEFINED_MESSAGE_NAME
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const messageContent = this.parsedJSON[message].message;
|
||||
let matches = regexp.exec(messageContent);
|
||||
while (matches !== null) {
|
||||
if (!this.hasPlaceholder(message, matches[1])) {
|
||||
this.collector.addWarning(Object.assign({
|
||||
file: this.filename,
|
||||
dataPath: `/${message}/placeholders/${matches[1]}`,
|
||||
}, messages.MISSING_PLACEHOLDER));
|
||||
this.collector.addWarning(
|
||||
Object.assign(
|
||||
{
|
||||
file: this.filename,
|
||||
dataPath: `/${message}/placeholders/${matches[1]}`,
|
||||
},
|
||||
messages.MISSING_PLACEHOLDER
|
||||
)
|
||||
);
|
||||
}
|
||||
matches = regexp.exec(messageContent);
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(this.parsedJSON[message], 'placeholders')) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
this.parsedJSON[message],
|
||||
'placeholders'
|
||||
)
|
||||
) {
|
||||
const visitedLowercasePlaceholders = [];
|
||||
Object.keys(this.parsedJSON[message].placeholders).forEach((placeholder) => {
|
||||
if (!visitedLowercasePlaceholders.includes(placeholder.toLowerCase())) {
|
||||
visitedLowercasePlaceholders.push(placeholder.toLowerCase());
|
||||
} else {
|
||||
this.collector.addError(Object.assign({}, messages.JSON_DUPLICATE_KEY, {
|
||||
file: this.filename,
|
||||
description: `Case-insensitive duplicate placeholder name: ${placeholder} found in JSON`,
|
||||
dataPath: `/${message}/placeholders/${placeholder}`,
|
||||
}));
|
||||
this.isValid = false;
|
||||
Object.keys(this.parsedJSON[message].placeholders).forEach(
|
||||
(placeholder) => {
|
||||
if (
|
||||
!visitedLowercasePlaceholders.includes(placeholder.toLowerCase())
|
||||
) {
|
||||
visitedLowercasePlaceholders.push(placeholder.toLowerCase());
|
||||
} else {
|
||||
this.collector.addError(
|
||||
Object.assign({}, messages.JSON_DUPLICATE_KEY, {
|
||||
file: this.filename,
|
||||
description: `Case-insensitive duplicate placeholder name: ${placeholder} found in JSON`,
|
||||
dataPath: `/${message}/placeholders/${placeholder}`,
|
||||
})
|
||||
);
|
||||
this.isValid = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
// Reset the regexp
|
||||
|
|
|
@ -9,9 +9,19 @@ import upath from 'upath';
|
|||
|
||||
import { getDefaultConfigValue } from 'yargs-options';
|
||||
import {
|
||||
validateAddon, validateDictionary, validateLangPack, validateStaticTheme,
|
||||
validateAddon,
|
||||
validateDictionary,
|
||||
validateLangPack,
|
||||
validateStaticTheme,
|
||||
} from 'schema/validator';
|
||||
import { MANIFEST_JSON, PACKAGE_EXTENSION, CSP_KEYWORD_RE, IMAGE_FILE_EXTENSIONS, LOCALES_DIRECTORY, MESSAGES_JSON } from 'const';
|
||||
import {
|
||||
MANIFEST_JSON,
|
||||
PACKAGE_EXTENSION,
|
||||
CSP_KEYWORD_RE,
|
||||
IMAGE_FILE_EXTENSIONS,
|
||||
LOCALES_DIRECTORY,
|
||||
MESSAGES_JSON,
|
||||
} from 'const';
|
||||
import log from 'logger';
|
||||
import * as messages from 'messages';
|
||||
import JSONParser from 'parsers/json';
|
||||
|
@ -19,7 +29,6 @@ import { isToolkitVersionString } from 'schema/formats';
|
|||
import { parseCspPolicy, normalizePath } from 'utils';
|
||||
import BLOCKED_CONTENT_SCRIPT_HOSTS from 'blocked_content_script_hosts.txt';
|
||||
|
||||
|
||||
async function getImageMetadata(io, iconPath) {
|
||||
// Get a non-utf8 input stream by setting encoding to null.
|
||||
// (only needed for the 'io/directory' module which open the file using the utf-8
|
||||
|
@ -34,13 +43,17 @@ async function getImageMetadata(io, iconPath) {
|
|||
return probeImageSize(data);
|
||||
}
|
||||
|
||||
|
||||
export default class ManifestJSONParser extends JSONParser {
|
||||
constructor(jsonString, collector, {
|
||||
filename = MANIFEST_JSON, RelaxedJSON = RJSON,
|
||||
selfHosted = getDefaultConfigValue('self-hosted'),
|
||||
io = null,
|
||||
} = {}) {
|
||||
constructor(
|
||||
jsonString,
|
||||
collector,
|
||||
{
|
||||
filename = MANIFEST_JSON,
|
||||
RelaxedJSON = RJSON,
|
||||
selfHosted = getDefaultConfigValue('self-hosted'),
|
||||
io = null,
|
||||
} = {}
|
||||
) {
|
||||
super(jsonString, collector, { filename });
|
||||
|
||||
this.parse(RelaxedJSON);
|
||||
|
@ -57,11 +70,17 @@ export default class ManifestJSONParser extends JSONParser {
|
|||
// We've parsed the JSON; now we can validate the manifest.
|
||||
this.selfHosted = selfHosted;
|
||||
this.isLanguagePack = Object.prototype.hasOwnProperty.call(
|
||||
this.parsedJSON, 'langpack_id');
|
||||
this.parsedJSON,
|
||||
'langpack_id'
|
||||
);
|
||||
this.isDictionary = Object.prototype.hasOwnProperty.call(
|
||||
this.parsedJSON, 'dictionaries');
|
||||
this.parsedJSON,
|
||||
'dictionaries'
|
||||
);
|
||||
this.isStaticTheme = Object.prototype.hasOwnProperty.call(
|
||||
this.parsedJSON, 'theme');
|
||||
this.parsedJSON,
|
||||
'theme'
|
||||
);
|
||||
this.io = io;
|
||||
this._validate();
|
||||
}
|
||||
|
@ -88,9 +107,11 @@ export default class ManifestJSONParser extends JSONParser {
|
|||
|
||||
if (error.keyword === 'required') {
|
||||
baseObject = messages.MANIFEST_FIELD_REQUIRED;
|
||||
} else if (error.dataPath.startsWith('/permissions') &&
|
||||
typeof error.data !== 'undefined' &&
|
||||
typeof error.data !== 'string') {
|
||||
} else if (
|
||||
error.dataPath.startsWith('/permissions') &&
|
||||
typeof error.data !== 'undefined' &&
|
||||
typeof error.data !== 'string'
|
||||
) {
|
||||
baseObject = messages.MANIFEST_BAD_PERMISSION;
|
||||
overrides.message = `Permissions ${error.message}.`;
|
||||
} else if (error.keyword === 'type') {
|
||||
|
@ -160,11 +181,17 @@ export default class ManifestJSONParser extends JSONParser {
|
|||
});
|
||||
}
|
||||
if (this.parsedJSON.background.page) {
|
||||
this.validateFileExistsInPackage(this.parsedJSON.background.page, 'page');
|
||||
this.validateFileExistsInPackage(
|
||||
this.parsedJSON.background.page,
|
||||
'page'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.parsedJSON.content_scripts && this.parsedJSON.content_scripts.length) {
|
||||
if (
|
||||
this.parsedJSON.content_scripts &&
|
||||
this.parsedJSON.content_scripts.length
|
||||
) {
|
||||
this.parsedJSON.content_scripts.forEach((scriptRule) => {
|
||||
if (scriptRule.matches && scriptRule.matches.length) {
|
||||
// Since `include_globs` only get's checked for patterns that are in
|
||||
|
@ -177,21 +204,27 @@ export default class ManifestJSONParser extends JSONParser {
|
|||
if (scriptRule.js && scriptRule.js.length) {
|
||||
scriptRule.js.forEach((script) => {
|
||||
this.validateFileExistsInPackage(
|
||||
script, 'script', messages.manifestContentScriptFileMissing);
|
||||
script,
|
||||
'script',
|
||||
messages.manifestContentScriptFileMissing
|
||||
);
|
||||
});
|
||||
}
|
||||
if (scriptRule.css && scriptRule.css.length) {
|
||||
scriptRule.css.forEach((style) => {
|
||||
this.validateFileExistsInPackage(
|
||||
style, 'css', messages.manifestContentScriptFileMissing);
|
||||
style,
|
||||
'css',
|
||||
messages.manifestContentScriptFileMissing
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.parsedJSON.dictionaries) {
|
||||
const numberOfDictionaries = Object.keys(
|
||||
this.parsedJSON.dictionaries).length;
|
||||
const numberOfDictionaries = Object.keys(this.parsedJSON.dictionaries)
|
||||
.length;
|
||||
if (numberOfDictionaries < 1) {
|
||||
this.collector.addError(messages.MANIFEST_EMPTY_DICTS);
|
||||
this.isValid = false;
|
||||
|
@ -202,25 +235,35 @@ export default class ManifestJSONParser extends JSONParser {
|
|||
Object.keys(this.parsedJSON.dictionaries).forEach((locale) => {
|
||||
const filepath = this.parsedJSON.dictionaries[locale];
|
||||
this.validateFileExistsInPackage(
|
||||
filepath, 'binary', messages.manifestDictionaryFileMissing);
|
||||
filepath,
|
||||
'binary',
|
||||
messages.manifestDictionaryFileMissing
|
||||
);
|
||||
// A corresponding .aff file should exist for every .dic.
|
||||
this.validateFileExistsInPackage(
|
||||
filepath.replace(/\.dic$/, '.aff'), 'binary',
|
||||
messages.manifestDictionaryFileMissing);
|
||||
filepath.replace(/\.dic$/, '.aff'),
|
||||
'binary',
|
||||
messages.manifestDictionaryFileMissing
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.selfHosted && this.parsedJSON.applications &&
|
||||
this.parsedJSON.applications.gecko &&
|
||||
this.parsedJSON.applications.gecko.update_url) {
|
||||
if (
|
||||
!this.selfHosted &&
|
||||
this.parsedJSON.applications &&
|
||||
this.parsedJSON.applications.gecko &&
|
||||
this.parsedJSON.applications.gecko.update_url
|
||||
) {
|
||||
this.collector.addError(messages.MANIFEST_UPDATE_URL);
|
||||
this.isValid = false;
|
||||
}
|
||||
|
||||
if (!this.isLanguagePack &&
|
||||
this.parsedJSON.applications &&
|
||||
this.parsedJSON.applications.gecko &&
|
||||
this.parsedJSON.applications.gecko.strict_max_version) {
|
||||
if (
|
||||
!this.isLanguagePack &&
|
||||
this.parsedJSON.applications &&
|
||||
this.parsedJSON.applications.gecko &&
|
||||
this.parsedJSON.applications.gecko.strict_max_version
|
||||
) {
|
||||
if (this.isDictionary) {
|
||||
// Dictionaries should not have a strict_max_version at all.
|
||||
this.isValid = false;
|
||||
|
@ -237,7 +280,10 @@ export default class ManifestJSONParser extends JSONParser {
|
|||
|
||||
if (this.parsedJSON.default_locale) {
|
||||
const msg = path.join(
|
||||
LOCALES_DIRECTORY, this.parsedJSON.default_locale, 'messages.json');
|
||||
LOCALES_DIRECTORY,
|
||||
this.parsedJSON.default_locale,
|
||||
'messages.json'
|
||||
);
|
||||
|
||||
// Convert filename to unix path separator before
|
||||
// searching it into the scanned files map.
|
||||
|
@ -265,13 +311,20 @@ export default class ManifestJSONParser extends JSONParser {
|
|||
if (existsSync(rootPath)) {
|
||||
readdirSync(rootPath).forEach((langDir) => {
|
||||
if (statSync(path.join(rootPath, langDir)).isDirectory()) {
|
||||
const filePath = path.join(LOCALES_DIRECTORY, langDir, MESSAGES_JSON);
|
||||
const filePath = path.join(
|
||||
LOCALES_DIRECTORY,
|
||||
langDir,
|
||||
MESSAGES_JSON
|
||||
);
|
||||
|
||||
// Convert filename to unix path separator before
|
||||
// searching it into the scanned files map.
|
||||
if (!this.io.files[upath.toUnix(filePath)]) {
|
||||
this.collector.addError(messages.noMessagesFileInLocales(
|
||||
path.join(LOCALES_DIRECTORY, langDir)));
|
||||
this.collector.addError(
|
||||
messages.noMessagesFileInLocales(
|
||||
path.join(LOCALES_DIRECTORY, langDir)
|
||||
)
|
||||
);
|
||||
this.isValid = false;
|
||||
}
|
||||
}
|
||||
|
@ -286,19 +339,28 @@ export default class ManifestJSONParser extends JSONParser {
|
|||
if (info.width !== info.height) {
|
||||
this.collector.addError(messages.iconIsNotSquare(iconPath));
|
||||
this.isValid = false;
|
||||
} else if (info.mime !== 'image/svg+xml' &&
|
||||
parseInt(info.width, 10) !== parseInt(expectedSize, 10)) {
|
||||
this.collector.addWarning(messages.iconSizeInvalid({
|
||||
path: iconPath,
|
||||
expected: parseInt(expectedSize, 10),
|
||||
actual: parseInt(info.width, 10),
|
||||
}));
|
||||
} else if (
|
||||
info.mime !== 'image/svg+xml' &&
|
||||
parseInt(info.width, 10) !== parseInt(expectedSize, 10)
|
||||
) {
|
||||
this.collector.addWarning(
|
||||
messages.iconSizeInvalid({
|
||||
path: iconPath,
|
||||
expected: parseInt(expectedSize, 10),
|
||||
actual: parseInt(info.width, 10),
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
log.debug(`Unexpected error raised while validating icon "${iconPath}"`, err);
|
||||
this.collector.addWarning(messages.corruptIconFile({
|
||||
path: iconPath,
|
||||
}));
|
||||
log.debug(
|
||||
`Unexpected error raised while validating icon "${iconPath}"`,
|
||||
err
|
||||
);
|
||||
this.collector.addWarning(
|
||||
messages.corruptIconFile({
|
||||
path: iconPath,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -310,7 +372,14 @@ export default class ManifestJSONParser extends JSONParser {
|
|||
if (!Object.prototype.hasOwnProperty.call(this.io.files, _path)) {
|
||||
this.collector.addError(messages.manifestIconMissing(_path));
|
||||
this.isValid = false;
|
||||
} else if (!IMAGE_FILE_EXTENSIONS.includes(_path.split('.').pop().toLowerCase())) {
|
||||
} else if (
|
||||
!IMAGE_FILE_EXTENSIONS.includes(
|
||||
_path
|
||||
.split('.')
|
||||
.pop()
|
||||
.toLowerCase()
|
||||
)
|
||||
) {
|
||||
this.collector.addWarning(messages.WRONG_ICON_EXTENSION);
|
||||
} else {
|
||||
promises.push(this.validateIcon(_path, size));
|
||||
|
@ -319,11 +388,14 @@ export default class ManifestJSONParser extends JSONParser {
|
|||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
validateFileExistsInPackage(filePath, type, messageFunc = messages.manifestBackgroundMissing) {
|
||||
validateFileExistsInPackage(
|
||||
filePath,
|
||||
type,
|
||||
messageFunc = messages.manifestBackgroundMissing
|
||||
) {
|
||||
const _path = normalizePath(filePath);
|
||||
if (!Object.prototype.hasOwnProperty.call(this.io.files, _path)) {
|
||||
this.collector.addError(messageFunc(
|
||||
_path, type));
|
||||
this.collector.addError(messageFunc(_path, type));
|
||||
this.isValid = false;
|
||||
}
|
||||
}
|
||||
|
@ -358,10 +430,12 @@ export default class ManifestJSONParser extends JSONParser {
|
|||
|
||||
// If the 'default-src' is insecure, check whether the 'script-src'
|
||||
// makes it secure, ie 'script-src: self;'
|
||||
if (insecureSrcDirective &&
|
||||
candidate === 'script-src' &&
|
||||
values.length === 1 &&
|
||||
values[0] === '\'self\'') {
|
||||
if (
|
||||
insecureSrcDirective &&
|
||||
candidate === 'script-src' &&
|
||||
values.length === 1 &&
|
||||
values[0] === "'self'"
|
||||
) {
|
||||
insecureSrcDirective = false;
|
||||
}
|
||||
|
||||
|
@ -373,9 +447,9 @@ export default class ManifestJSONParser extends JSONParser {
|
|||
continue;
|
||||
}
|
||||
|
||||
const hasProtocol = (
|
||||
const hasProtocol =
|
||||
(value.endsWith(':') && validProtocols.includes(value)) ||
|
||||
(validProtocols.some((x) => value.startsWith(x))));
|
||||
validProtocols.some((x) => value.startsWith(x));
|
||||
|
||||
if (hasProtocol) {
|
||||
if (candidate === 'default-src') {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
export default class PropertiesParser {
|
||||
/*
|
||||
* Parser for .properties files.
|
||||
|
@ -30,7 +29,8 @@ export default class PropertiesParser {
|
|||
// Skip empty lines and comments
|
||||
if (!cleanedLine) {
|
||||
return;
|
||||
} else if (cleanedLine.startsWith('#')) {
|
||||
}
|
||||
if (cleanedLine.startsWith('#')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import * as messages from 'messages';
|
||||
|
||||
|
||||
export function invalidNesting(cssNode, filename,
|
||||
{ startLine, startColumn } = {}) {
|
||||
export function invalidNesting(
|
||||
cssNode,
|
||||
filename,
|
||||
{ startLine, startColumn } = {}
|
||||
) {
|
||||
const messageList = [];
|
||||
if (cssNode.type === 'rule') {
|
||||
for (let i = 0; i < cssNode.nodes.length; i++) {
|
||||
|
@ -14,7 +16,8 @@ export function invalidNesting(cssNode, filename,
|
|||
line: startLine,
|
||||
column: startColumn,
|
||||
file: filename,
|
||||
}));
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { VALIDATION_WARNING } from 'const';
|
||||
import * as messages from 'messages';
|
||||
|
||||
|
||||
export async function warnOnInline($, filename) {
|
||||
const linterMessages = [];
|
||||
$('script').each((i, element) => {
|
||||
if ($(element).attr('src') === undefined &&
|
||||
($(element).attr('type') === undefined ||
|
||||
$(element).attr('type') === 'text/javascript')) {
|
||||
if (
|
||||
$(element).attr('src') === undefined &&
|
||||
($(element).attr('type') === undefined ||
|
||||
$(element).attr('type') === 'text/javascript')
|
||||
) {
|
||||
linterMessages.push(
|
||||
Object.assign({}, messages.INLINE_SCRIPT, {
|
||||
/* This could occur in any HTML file, so let's make it
|
||||
|
@ -15,7 +16,8 @@ export async function warnOnInline($, filename) {
|
|||
*/
|
||||
type: VALIDATION_WARNING,
|
||||
file: filename,
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import { VALIDATION_WARNING } from 'const';
|
|||
import { isStrictRelativeUrl } from 'schema/formats';
|
||||
import * as messages from 'messages';
|
||||
|
||||
|
||||
export async function warnOnRemoteScript($, filename) {
|
||||
const linterMessages = [];
|
||||
|
||||
|
|
|
@ -1,19 +1,25 @@
|
|||
import * as path from 'path';
|
||||
|
||||
import { isBrowserNamespace } from 'utils';
|
||||
import { CONTENT_SCRIPT_NOT_FOUND, CONTENT_SCRIPT_EMPTY } from 'messages/javascript';
|
||||
import {
|
||||
CONTENT_SCRIPT_NOT_FOUND,
|
||||
CONTENT_SCRIPT_EMPTY,
|
||||
} from 'messages/javascript';
|
||||
|
||||
export default {
|
||||
create(context) {
|
||||
const existingFiles = Object.keys(
|
||||
context.settings.existingFiles || {}
|
||||
).map((fileName) => {
|
||||
return path.resolve('/', fileName);
|
||||
});
|
||||
const existingFiles = Object.keys(context.settings.existingFiles || {}).map(
|
||||
(fileName) => {
|
||||
return path.resolve('/', fileName);
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
if (!node.object.object || !isBrowserNamespace(node.object.object.name)) {
|
||||
if (
|
||||
!node.object.object ||
|
||||
!isBrowserNamespace(node.object.object.name)
|
||||
) {
|
||||
// Early return when it's not our case.
|
||||
return;
|
||||
}
|
||||
|
@ -21,7 +27,11 @@ export default {
|
|||
const property = node.property.name;
|
||||
// Namespace should be tabs function should be executeScript and it should be a call.
|
||||
// I.E. browser.tabs.executeScript().
|
||||
if (namespace !== 'tabs' || property !== 'executeScript' || node.parent.type !== 'CallExpression') {
|
||||
if (
|
||||
namespace !== 'tabs' ||
|
||||
property !== 'executeScript' ||
|
||||
node.parent.type !== 'CallExpression'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
node.parent.arguments.forEach((arg) => {
|
||||
|
@ -29,10 +39,17 @@ export default {
|
|||
if (arg.type !== 'ObjectExpression') {
|
||||
return;
|
||||
}
|
||||
const fileProperty = arg.properties.find((prop) => prop.key && prop.key.name === 'file');
|
||||
const fileValue = fileProperty && fileProperty.value && fileProperty.value.value;
|
||||
const fileProperty = arg.properties.find(
|
||||
(prop) => prop.key && prop.key.name === 'file'
|
||||
);
|
||||
const fileValue =
|
||||
fileProperty && fileProperty.value && fileProperty.value.value;
|
||||
// Skipping the argument if there is no file property, or value is not a static string.
|
||||
if (!fileProperty || fileProperty.value.type !== 'Literal' || typeof fileValue !== 'string') {
|
||||
if (
|
||||
!fileProperty ||
|
||||
fileProperty.value.type !== 'Literal' ||
|
||||
typeof fileValue !== 'string'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// If filename is empty, report an issue.
|
||||
|
|
|
@ -9,7 +9,6 @@ export const DEPRECATED_ENTITIES = [
|
|||
},
|
||||
];
|
||||
|
||||
|
||||
export default {
|
||||
create(context) {
|
||||
return {
|
||||
|
@ -17,16 +16,23 @@ export default {
|
|||
CallExpression(node) {
|
||||
const referenceNode = getNodeReference(context, node.callee);
|
||||
// We're only looking for calls that look like `foo.bar()`.
|
||||
if (typeof referenceNode.object !== 'undefined' &&
|
||||
referenceNode.property.type === 'Identifier' &&
|
||||
referenceNode.object.type === 'Identifier') {
|
||||
const referenceObject = getNodeReference(context, referenceNode.object);
|
||||
if (
|
||||
typeof referenceNode.object !== 'undefined' &&
|
||||
referenceNode.property.type === 'Identifier' &&
|
||||
referenceNode.object.type === 'Identifier'
|
||||
) {
|
||||
const referenceObject = getNodeReference(
|
||||
context,
|
||||
referenceNode.object
|
||||
);
|
||||
|
||||
for (let i = 0; i < DEPRECATED_ENTITIES.length; i++) {
|
||||
const entity = DEPRECATED_ENTITIES[i];
|
||||
// Check to see if the node matches a deprecated entity.
|
||||
if (referenceObject.name === entity.object &&
|
||||
referenceNode.property.name === entity.property) {
|
||||
if (
|
||||
referenceObject.name === entity.object &&
|
||||
referenceNode.property.name === entity.property
|
||||
) {
|
||||
return context.report(node, entity.error.code);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,9 +7,11 @@ export default {
|
|||
// eslint-disable-next-line consistent-return
|
||||
CallExpression(node) {
|
||||
let referenceNode = getNodeReference(context, node.callee);
|
||||
if (typeof referenceNode.property !== 'undefined' &&
|
||||
referenceNode.property.type === 'Identifier' &&
|
||||
referenceNode.property.name === 'addEventListener') {
|
||||
if (
|
||||
typeof referenceNode.property !== 'undefined' &&
|
||||
referenceNode.property.type === 'Identifier' &&
|
||||
referenceNode.property.name === 'addEventListener'
|
||||
) {
|
||||
if (node.arguments.length > 3) {
|
||||
const wantsUntrusted = node.arguments[3];
|
||||
if (wantsUntrusted.type === 'Literal') {
|
||||
|
|
|
@ -10,9 +10,11 @@ export default {
|
|||
return {
|
||||
// eslint-disable-next-line consistent-return
|
||||
CallExpression(node) {
|
||||
if (node.callee.name === 'require' &&
|
||||
node.arguments &&
|
||||
node.arguments.length) {
|
||||
if (
|
||||
node.callee.name === 'require' &&
|
||||
node.arguments &&
|
||||
node.arguments.length
|
||||
) {
|
||||
const firstArg = node.arguments[0];
|
||||
if (firstArg.type === 'Identifier') {
|
||||
const pathVar = getVariable(context, firstArg.name);
|
||||
|
|
|
@ -10,8 +10,9 @@ module.exports = {
|
|||
'opendialog-nonlit-uri': require('./opendialog-nonlit-uri').default,
|
||||
'opendialog-remote-uri': require('./opendialog-remote-uri').default,
|
||||
'webextension-api': require('./webextension-api').default,
|
||||
'content-scripts-file-absent': require('./content-scripts-file-absent').default,
|
||||
'webextension-unsupported-api':
|
||||
require('./webextension-unsupported-api').default,
|
||||
'content-scripts-file-absent': require('./content-scripts-file-absent')
|
||||
.default,
|
||||
'webextension-unsupported-api': require('./webextension-unsupported-api')
|
||||
.default,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import { OPENDIALOG_NONLIT_URI } from 'messages';
|
||||
|
||||
|
||||
export default {
|
||||
create(context) {
|
||||
return {
|
||||
// eslint-disable-next-line consistent-return
|
||||
CallExpression(node) {
|
||||
if (node.callee.type === 'MemberExpression' &&
|
||||
node.callee.property.type === 'Identifier' &&
|
||||
node.callee.property.name === 'openDialog') {
|
||||
if (
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
node.callee.property.type === 'Identifier' &&
|
||||
node.callee.property.name === 'openDialog'
|
||||
) {
|
||||
if (node.arguments.length) {
|
||||
const uri = node.arguments[0];
|
||||
if (uri.type !== 'Literal') {
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import { isLocalUrl } from 'utils';
|
||||
import { OPENDIALOG_REMOTE_URI } from 'messages';
|
||||
|
||||
|
||||
export default {
|
||||
create(context) {
|
||||
return {
|
||||
// eslint-disable-next-line consistent-return
|
||||
CallExpression(node) {
|
||||
if (node.callee.type === 'MemberExpression' &&
|
||||
node.callee.property.type === 'Identifier' &&
|
||||
node.callee.property.name === 'openDialog') {
|
||||
if (
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
node.callee.property.type === 'Identifier' &&
|
||||
node.callee.property.name === 'openDialog'
|
||||
) {
|
||||
if (node.arguments.length) {
|
||||
const uri = node.arguments[0];
|
||||
if (uri.type === 'Literal' &&
|
||||
isLocalUrl(uri.value) === false) {
|
||||
if (uri.type === 'Literal' && isLocalUrl(uri.value) === false) {
|
||||
return context.report(node, OPENDIALOG_REMOTE_URI.code);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,10 @@ export default {
|
|||
return context.report(node, apiToMessage(api));
|
||||
}
|
||||
|
||||
if (!context.settings.addonMetadata.id &&
|
||||
isTemporaryApi(namespace, property)) {
|
||||
if (
|
||||
!context.settings.addonMetadata.id &&
|
||||
isTemporaryApi(namespace, property)
|
||||
) {
|
||||
return context.report(node, apiToMessage(api));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,11 @@ export default {
|
|||
create(context) {
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
if (!node.computed &&
|
||||
node.object.object &&
|
||||
isBrowserNamespace(node.object.object.name)) {
|
||||
if (
|
||||
!node.computed &&
|
||||
node.object.object &&
|
||||
isBrowserNamespace(node.object.object.name)
|
||||
) {
|
||||
const namespace = node.object.property.name;
|
||||
const property = node.property.name;
|
||||
const api = `${namespace}.${property}`;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { ensureFilenameExists, ignorePrivateFunctions } from 'utils';
|
||||
|
||||
|
||||
export default class BaseScanner {
|
||||
static get fileResultType() {
|
||||
/*
|
||||
|
@ -49,10 +48,12 @@ export default class BaseScanner {
|
|||
// to include them in our linter's rules.)
|
||||
const rules = ignorePrivateFunctions(_rules);
|
||||
|
||||
const ruleResults = await Promise.all(Object.keys(rules).map((rule) => {
|
||||
this._rulesProcessed++;
|
||||
return rules[rule](contents, this.filename, this.options);
|
||||
}));
|
||||
const ruleResults = await Promise.all(
|
||||
Object.keys(rules).map((rule) => {
|
||||
this._rulesProcessed++;
|
||||
return rules[rule](contents, this.filename, this.options);
|
||||
})
|
||||
);
|
||||
|
||||
ruleResults.forEach((messages) => {
|
||||
this.linterMessages = this.linterMessages.concat(messages);
|
||||
|
|
|
@ -2,7 +2,6 @@ import BaseScanner from 'scanners/base';
|
|||
import * as messages from 'messages';
|
||||
import * as constants from 'const';
|
||||
|
||||
|
||||
export default class BinaryScanner extends BaseScanner {
|
||||
static get fileStreamType() {
|
||||
return 'chunk';
|
||||
|
|
|
@ -7,7 +7,6 @@ import { VALIDATION_WARNING } from 'const';
|
|||
import { ignorePrivateFunctions } from 'utils';
|
||||
import * as cssRules from 'rules/css';
|
||||
|
||||
|
||||
export default class CSSScanner extends BaseScanner {
|
||||
_defaultRules = cssRules;
|
||||
|
||||
|
@ -45,11 +44,11 @@ export default class CSSScanner extends BaseScanner {
|
|||
return;
|
||||
}
|
||||
|
||||
log.debug('Passing CSS code to rule function "%s"',
|
||||
cssInstruction, info);
|
||||
log.debug('Passing CSS code to rule function "%s"', cssInstruction, info);
|
||||
|
||||
this.linterMessages = this.linterMessages.concat(
|
||||
_rules[cssInstruction](cssNode, file, cssOptions));
|
||||
_rules[cssInstruction](cssNode, file, cssOptions)
|
||||
);
|
||||
}
|
||||
|
||||
async scan(_rules = this._defaultRules) {
|
||||
|
@ -80,17 +79,19 @@ export default class CSSScanner extends BaseScanner {
|
|||
throw e;
|
||||
}
|
||||
|
||||
this.linterMessages.push(Object.assign({}, CSS_SYNTAX_ERROR, {
|
||||
type: VALIDATION_WARNING,
|
||||
// Use the reason for the error as the message.
|
||||
// e.message includes an absolute path.
|
||||
message: e.reason,
|
||||
column: e.column,
|
||||
line: e.line,
|
||||
// We use our own ref to the file as postcss outputs
|
||||
// absolute paths.
|
||||
file: this.filename,
|
||||
}));
|
||||
this.linterMessages.push(
|
||||
Object.assign({}, CSS_SYNTAX_ERROR, {
|
||||
type: VALIDATION_WARNING,
|
||||
// Use the reason for the error as the message.
|
||||
// e.message includes an absolute path.
|
||||
message: e.reason,
|
||||
column: e.column,
|
||||
line: e.line,
|
||||
// We use our own ref to the file as postcss outputs
|
||||
// absolute paths.
|
||||
file: this.filename,
|
||||
})
|
||||
);
|
||||
|
||||
// A syntax error has been encounted so it's game over.
|
||||
return null;
|
||||
|
|
|
@ -4,7 +4,6 @@ import BaseScanner from 'scanners/base';
|
|||
import * as messages from 'messages';
|
||||
import * as constants from 'const';
|
||||
|
||||
|
||||
export default class FilenameScanner extends BaseScanner {
|
||||
static get scannerName() {
|
||||
return 'filename';
|
||||
|
|
|
@ -3,7 +3,6 @@ import cheerio from 'cheerio';
|
|||
import BaseScanner from 'scanners/base';
|
||||
import * as rules from 'rules/html';
|
||||
|
||||
|
||||
export default class HTMLScanner extends BaseScanner {
|
||||
_defaultRules = rules;
|
||||
|
||||
|
|
|
@ -3,18 +3,13 @@ import { oneLine } from 'common-tags';
|
|||
import espree from 'espree';
|
||||
import vk from 'eslint-visitor-keys';
|
||||
|
||||
import {
|
||||
ESLINT_RULE_MAPPING,
|
||||
ESLINT_TYPES,
|
||||
} from 'const';
|
||||
import { ESLINT_RULE_MAPPING, ESLINT_TYPES } from 'const';
|
||||
import * as messages from 'messages';
|
||||
import { rules } from 'rules/javascript';
|
||||
import { ensureFilenameExists } from 'utils';
|
||||
|
||||
|
||||
const ECMA_VERSION = 2019;
|
||||
|
||||
|
||||
export function excludeRules(excludeFrom = {}, excludeWhat = []) {
|
||||
return Object.keys(excludeFrom).reduce((result, ruleName) => {
|
||||
if (excludeWhat.includes(ruleName)) return result;
|
||||
|
@ -27,6 +22,7 @@ export function excludeRules(excludeFrom = {}, excludeWhat = []) {
|
|||
|
||||
export default class JavaScriptScanner {
|
||||
_defaultRules = rules;
|
||||
|
||||
disabledRules = [];
|
||||
|
||||
constructor(code, filename, options = {}) {
|
||||
|
@ -36,9 +32,13 @@ export default class JavaScriptScanner {
|
|||
this.linterMessages = [];
|
||||
this.scannedFiles = [];
|
||||
this._rulesProcessed = 0;
|
||||
this.disabledRules = typeof options.disabledRules === 'string' ? options.disabledRules.split(',')
|
||||
.map((rule) => rule.trim())
|
||||
.filter((notEmptyRule) => notEmptyRule) : [];
|
||||
this.disabledRules =
|
||||
typeof options.disabledRules === 'string'
|
||||
? options.disabledRules
|
||||
.split(',')
|
||||
.map((rule) => rule.trim())
|
||||
.filter((notEmptyRule) => notEmptyRule)
|
||||
: [];
|
||||
ensureFilenameExists(this.filename);
|
||||
}
|
||||
|
||||
|
@ -50,11 +50,14 @@ export default class JavaScriptScanner {
|
|||
return 'javascript';
|
||||
}
|
||||
|
||||
async scan(_ESLint = ESLint, {
|
||||
_rules = this._defaultRules,
|
||||
_ruleMapping = ESLINT_RULE_MAPPING,
|
||||
_messages = messages,
|
||||
} = {}) {
|
||||
async scan(
|
||||
_ESLint = ESLint,
|
||||
{
|
||||
_rules = this._defaultRules,
|
||||
_ruleMapping = ESLINT_RULE_MAPPING,
|
||||
_messages = messages,
|
||||
} = {}
|
||||
) {
|
||||
this._ESLint = ESLint;
|
||||
this.sourceType = this.detectSourceType(this.filename);
|
||||
|
||||
|
@ -121,7 +124,8 @@ export default class JavaScriptScanner {
|
|||
if (typeof message.message === 'undefined') {
|
||||
throw new Error(
|
||||
oneLine`JS rules must pass a valid message as
|
||||
the second argument to context.report()`);
|
||||
the second argument to context.report()`
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback to looking up the message object by the message
|
||||
|
@ -133,12 +137,13 @@ export default class JavaScriptScanner {
|
|||
// message structure and allow us to optionally overwrite
|
||||
// their `message` and `description`.
|
||||
if (Object.prototype.hasOwnProperty.call(_messages, code)) {
|
||||
({
|
||||
message: shortDescription,
|
||||
description,
|
||||
} = _messages[code]);
|
||||
} else if (Object.prototype.hasOwnProperty.call(
|
||||
messages.ESLINT_OVERWRITE_MESSAGE, message.ruleId)) {
|
||||
({ message: shortDescription, description } = _messages[code]);
|
||||
} else if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
messages.ESLINT_OVERWRITE_MESSAGE,
|
||||
message.ruleId
|
||||
)
|
||||
) {
|
||||
const overwrites = messages.ESLINT_OVERWRITE_MESSAGE[message.ruleId];
|
||||
shortDescription = overwrites.message || message.message;
|
||||
description = overwrites.description || message.description;
|
||||
|
@ -172,10 +177,14 @@ export default class JavaScriptScanner {
|
|||
|
||||
_getSourceType(node) {
|
||||
const possibleImportExportTypes = [
|
||||
'ExportAllDeclaration', 'ExportDefaultDeclaration',
|
||||
'ExportNamedDeclaration', 'ExportSpecifier',
|
||||
'ImportDeclaration', 'ImportDefaultSpecifier',
|
||||
'ImportNamespaceSpecifier', 'ImportSpecifier',
|
||||
'ExportAllDeclaration',
|
||||
'ExportDefaultDeclaration',
|
||||
'ExportNamedDeclaration',
|
||||
'ExportSpecifier',
|
||||
'ImportDeclaration',
|
||||
'ImportDefaultSpecifier',
|
||||
'ImportNamespaceSpecifier',
|
||||
'ImportSpecifier',
|
||||
];
|
||||
|
||||
if (possibleImportExportTypes.includes(node.type)) {
|
||||
|
|
|
@ -3,7 +3,6 @@ import LocaleMessagesJSONParser from 'parsers/locale-messagesjson';
|
|||
import BaseScanner from 'scanners/base';
|
||||
import { MESSAGES_JSON, LOCALES_DIRECTORY } from 'const';
|
||||
|
||||
|
||||
export default class JSONScanner extends BaseScanner {
|
||||
static get scannerName() {
|
||||
return 'json';
|
||||
|
@ -16,7 +15,10 @@ export default class JSONScanner extends BaseScanner {
|
|||
async scan() {
|
||||
const json = await this.getContents();
|
||||
|
||||
if (this.filename.endsWith(MESSAGES_JSON) && this.filename.startsWith(LOCALES_DIRECTORY)) {
|
||||
if (
|
||||
this.filename.endsWith(MESSAGES_JSON) &&
|
||||
this.filename.startsWith(LOCALES_DIRECTORY)
|
||||
) {
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(
|
||||
json,
|
||||
this.options.collector,
|
||||
|
@ -24,11 +26,9 @@ export default class JSONScanner extends BaseScanner {
|
|||
);
|
||||
localeMessagesJSONParser.parse();
|
||||
} else {
|
||||
const jsonParser = new JSONParser(
|
||||
json,
|
||||
this.options.collector,
|
||||
{ filename: this.filename }
|
||||
);
|
||||
const jsonParser = new JSONParser(json, this.options.collector, {
|
||||
filename: this.filename,
|
||||
});
|
||||
jsonParser.parse();
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import PropertiesParser from 'parsers/properties';
|
|||
import DoctypeParser from 'parsers/doctype';
|
||||
import BaseScanner from 'scanners/base';
|
||||
|
||||
|
||||
export default class LangpackScanner extends BaseScanner {
|
||||
static get scannerName() {
|
||||
return 'langpack';
|
||||
|
|
|
@ -3,10 +3,13 @@ import schemaList from 'schema/imported';
|
|||
|
||||
const schemaArrayNames = ['functions', 'events'];
|
||||
const schemaObjectNames = ['types', 'properties'];
|
||||
const schemas = schemaList.reduce((all, current) => ({
|
||||
...all,
|
||||
[current.id]: current,
|
||||
}), {});
|
||||
const schemas = schemaList.reduce(
|
||||
(all, current) => ({
|
||||
...all,
|
||||
[current.id]: current,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
export function isDeprecatedApi(namespace, property) {
|
||||
return DEPRECATED_APIS.includes(`${namespace}.${property}`);
|
||||
|
@ -25,10 +28,12 @@ function hasObjectProperty(schema, property) {
|
|||
function hasArrayProperty(schema, property) {
|
||||
return schemaArrayNames.some((schemaProperty) => {
|
||||
const namespaceProperties = schema[schemaProperty];
|
||||
return Array.isArray(namespaceProperties) &&
|
||||
return (
|
||||
Array.isArray(namespaceProperties) &&
|
||||
namespaceProperties.some((schemaItem) => {
|
||||
return schemaItem.name === property;
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -36,13 +41,16 @@ export function hasBrowserApi(namespace, property) {
|
|||
const schema = schemas[namespace];
|
||||
// We "have" the API if it's deprecated or temporary so
|
||||
// we don't double warn.
|
||||
if (isDeprecatedApi(namespace, property)
|
||||
|| isTemporaryApi(namespace, property)) {
|
||||
if (
|
||||
isDeprecatedApi(namespace, property) ||
|
||||
isTemporaryApi(namespace, property)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (!schema) {
|
||||
return false;
|
||||
}
|
||||
return hasObjectProperty(schema, property)
|
||||
|| hasArrayProperty(schema, property);
|
||||
return (
|
||||
hasObjectProperty(schema, property) || hasArrayProperty(schema, property)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,8 @@ function oldArrayMerge(target, source, optionsArgument) {
|
|||
|
||||
source.forEach((e, i) => {
|
||||
if (typeof destination[i] === 'undefined') {
|
||||
const cloneRequested = !optionsArgument || optionsArgument.clone !== false;
|
||||
const cloneRequested =
|
||||
!optionsArgument || optionsArgument.clone !== false;
|
||||
const shouldClone = cloneRequested && isMergeableObject(e);
|
||||
destination[i] = shouldClone ? clone(e, optionsArgument) : e;
|
||||
} else if (isMergeableObject(e)) {
|
||||
|
@ -33,7 +34,9 @@ function oldArrayMerge(target, source, optionsArgument) {
|
|||
|
||||
export default (a, b, opts) => {
|
||||
if (opts) {
|
||||
throw new Error('opts are not supported, use the deepmerge package directly');
|
||||
throw new Error(
|
||||
'opts are not supported, use the deepmerge package directly'
|
||||
);
|
||||
}
|
||||
return merge(a, b, { arrayMerge: oldArrayMerge });
|
||||
};
|
||||
|
|
|
@ -98,9 +98,11 @@ export function rewriteOptionalToRequired(schema) {
|
|||
function isUnrecognizedProperty(value) {
|
||||
if (typeof value === 'object') {
|
||||
const keys = Object.keys(value);
|
||||
return keys.length === 1
|
||||
&& '$ref' in value
|
||||
&& UNRECOGNIZED_PROPERTY_REFS.includes(value.$ref);
|
||||
return (
|
||||
keys.length === 1 &&
|
||||
'$ref' in value &&
|
||||
UNRECOGNIZED_PROPERTY_REFS.includes(value.$ref)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -117,10 +119,11 @@ function rewriteIdRef(value, namespace = '') {
|
|||
export function rewriteValue(key, value, namespace) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((val) => rewriteValue(key, val, namespace));
|
||||
} else if (key === 'additionalProperties' &&
|
||||
isUnrecognizedProperty(value)) {
|
||||
}
|
||||
if (key === 'additionalProperties' && isUnrecognizedProperty(value)) {
|
||||
return undefined;
|
||||
} else if (typeof value === 'object') {
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
if ('$ref' in value && Object.keys(value).length > 1) {
|
||||
const { $ref, ...rest } = value;
|
||||
if (Object.keys(rest).length === 1 && 'optional' in rest) {
|
||||
|
@ -139,25 +142,31 @@ export function rewriteValue(key, value, namespace) {
|
|||
const rewritten = inner.rewriteObject(value, namespace);
|
||||
if ('properties' in rewritten) {
|
||||
const { required, ...properties } = rewriteOptionalToRequired(
|
||||
rewritten.properties);
|
||||
rewritten.properties
|
||||
);
|
||||
if (required.length > 0) {
|
||||
return { ...rewritten, properties, required };
|
||||
}
|
||||
return { ...rewritten, properties };
|
||||
}
|
||||
return rewritten;
|
||||
} else if (key === '$ref') {
|
||||
}
|
||||
if (key === '$ref') {
|
||||
if (value.includes('#/types')) {
|
||||
return value;
|
||||
} else if (value in refMap) {
|
||||
}
|
||||
if (value in refMap) {
|
||||
return refMap[value];
|
||||
}
|
||||
return rewriteIdRef(value);
|
||||
} else if (key === 'type' && value === 'any') {
|
||||
}
|
||||
if (key === 'type' && value === 'any') {
|
||||
return undefined;
|
||||
} else if (key === 'id') {
|
||||
}
|
||||
if (key === 'id') {
|
||||
return undefined;
|
||||
} else if (key === 'pattern') {
|
||||
}
|
||||
if (key === 'pattern') {
|
||||
return rewritePatternFlags(value);
|
||||
}
|
||||
return value;
|
||||
|
@ -255,17 +264,22 @@ inner.updateWithAddonsLinterData = (firefoxSchemas, ourSchemas) => {
|
|||
|
||||
export function loadTypes(types = []) {
|
||||
// Convert the array of types to an object.
|
||||
return types.reduce((obj, type) => ({
|
||||
...obj,
|
||||
[type.id]: type,
|
||||
}), {});
|
||||
return types.reduce(
|
||||
(obj, type) => ({
|
||||
...obj,
|
||||
[type.id]: type,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
function rewriteExtendRefs(definition, namespace, types) {
|
||||
if (Array.isArray(definition)) {
|
||||
return definition.map(
|
||||
(value) => rewriteExtendRefs(value, namespace, types));
|
||||
} else if (typeof definition === 'object') {
|
||||
return definition.map((value) =>
|
||||
rewriteExtendRefs(value, namespace, types)
|
||||
);
|
||||
}
|
||||
if (typeof definition === 'object') {
|
||||
return Object.keys(definition).reduce((obj, key) => {
|
||||
const value = definition[key];
|
||||
if (key === '$ref') {
|
||||
|
@ -361,8 +375,10 @@ export function foldSchemas(schemas) {
|
|||
const prefixedSchemas = schemasByPrefix[prefix];
|
||||
// Continue if there are multiple properties (baseNamespace and something
|
||||
// else) or there is one property that isn't baseNamespace.
|
||||
return Object.keys(prefixedSchemas).length > 1
|
||||
|| !('baseNamespace' in prefixedSchemas);
|
||||
return (
|
||||
Object.keys(prefixedSchemas).length > 1 ||
|
||||
!('baseNamespace' in prefixedSchemas)
|
||||
);
|
||||
});
|
||||
if (!hasMatchingPrefixes) {
|
||||
return schemas;
|
||||
|
@ -403,14 +419,16 @@ inner.normalizeSchema = (schemas, file) => {
|
|||
|
||||
if (filteredSchemas.length === 1) {
|
||||
// If there is only a manifest namespace then this just extends the manifest.
|
||||
if (filteredSchemas[0].namespace === 'manifest'
|
||||
&& file !== 'manifest.json') {
|
||||
if (
|
||||
filteredSchemas[0].namespace === 'manifest' &&
|
||||
file !== 'manifest.json'
|
||||
) {
|
||||
primarySchema = {
|
||||
namespace: file.slice(0, file.indexOf('.')),
|
||||
};
|
||||
extendSchemas = [filteredSchemas[0]];
|
||||
} else {
|
||||
([primarySchema] = filteredSchemas);
|
||||
[primarySchema] = filteredSchemas;
|
||||
extendSchemas = [];
|
||||
}
|
||||
} else {
|
||||
|
@ -419,7 +437,9 @@ inner.normalizeSchema = (schemas, file) => {
|
|||
}
|
||||
const { namespace, types, ...rest } = primarySchema;
|
||||
const { types: extendTypes, ...extendRest } = rewriteExtend(
|
||||
extendSchemas, namespace);
|
||||
extendSchemas,
|
||||
namespace
|
||||
);
|
||||
const updatedTypes = { ...loadTypes(types), ...extendTypes };
|
||||
return {
|
||||
...rest,
|
||||
|
@ -439,7 +459,7 @@ inner.mergeSchemas = (schemaLists) => {
|
|||
Object.keys(schemaLists).forEach((namespace) => {
|
||||
const namespaceSchemas = schemaLists[namespace];
|
||||
if (namespaceSchemas.length === 1) {
|
||||
([schemas[namespace]] = namespaceSchemas);
|
||||
[schemas[namespace]] = namespaceSchemas;
|
||||
} else {
|
||||
const file = `${namespace}.json`;
|
||||
const merged = namespaceSchemas.reduce((memo, { schema }) => {
|
||||
|
@ -468,22 +488,21 @@ export function processSchemas(schemas) {
|
|||
return inner.mapExtendToRef(mergedSchemasByNamespace);
|
||||
}
|
||||
|
||||
const SKIP_SCHEMAS = [
|
||||
'native_host_manifest.json',
|
||||
];
|
||||
const SKIP_SCHEMAS = ['native_host_manifest.json'];
|
||||
|
||||
function readSchema(basePath, file) {
|
||||
return commentJson.parse(
|
||||
fs.readFileSync(path.join(basePath, file), 'utf-8'),
|
||||
null, // reviver
|
||||
true, // remove_comments
|
||||
true // remove_comments
|
||||
);
|
||||
}
|
||||
|
||||
function writeSchema(basePath, file, schema) {
|
||||
fs.writeFileSync(
|
||||
path.join(basePath, file),
|
||||
`${JSON.stringify(schema, undefined, 2)}\n`);
|
||||
`${JSON.stringify(schema, undefined, 2)}\n`
|
||||
);
|
||||
}
|
||||
|
||||
function schemaFiles(basePath) {
|
||||
|
@ -498,16 +517,25 @@ function writeSchemasToFile(basePath, importedPath, loadedSchemas) {
|
|||
writeSchema(importedPath, file, schema);
|
||||
});
|
||||
// Write out the index.js to easily import all schemas.
|
||||
const imports = ids.filter((id) => { return id !== 'manifest'; }).map((id) => {
|
||||
const { file } = loadedSchemas[id];
|
||||
const basename = path.basename(file);
|
||||
return `import ${id} from './${basename}'`;
|
||||
}).join(';\n');
|
||||
const imports = ids
|
||||
.filter((id) => {
|
||||
return id !== 'manifest';
|
||||
})
|
||||
.map((id) => {
|
||||
const { file } = loadedSchemas[id];
|
||||
const basename = path.basename(file);
|
||||
return `import ${id} from './${basename}'`;
|
||||
})
|
||||
.join(';\n');
|
||||
const fileContents = `// This file is generated by the schema import script.
|
||||
|
||||
${imports};
|
||||
export default [
|
||||
${ids.filter((id) => { return id !== 'manifest'; }).join(',\n ')},
|
||||
${ids
|
||||
.filter((id) => {
|
||||
return id !== 'manifest';
|
||||
})
|
||||
.join(',\n ')},
|
||||
];
|
||||
`;
|
||||
fs.writeFileSync(path.join(importedPath, 'index.js'), fileContents);
|
||||
|
@ -534,7 +562,9 @@ export function importSchemas(firefoxPath, ourPath, importedPath) {
|
|||
};
|
||||
const processedSchemas = processSchemas(rawSchemas);
|
||||
const updatedSchemas = inner.updateWithAddonsLinterData(
|
||||
processedSchemas, ourSchemas);
|
||||
processedSchemas,
|
||||
ourSchemas
|
||||
);
|
||||
writeSchemasToFile(firefoxPath, importedPath, updatedSchemas);
|
||||
}
|
||||
|
||||
|
|
|
@ -101,9 +101,11 @@ export function isSecureUrl(value) {
|
|||
export function imageDataOrStrictRelativeUrl(value) {
|
||||
// Do not accept a string which resolves as an absolute URL, or any
|
||||
// protocol-relative URL, except PNG or JPG data URLs.
|
||||
return value.startsWith('data:image/png;base64,')
|
||||
|| value.startsWith('data:image/jpeg;base64,')
|
||||
|| isStrictRelativeUrl(value);
|
||||
return (
|
||||
value.startsWith('data:image/png;base64,') ||
|
||||
value.startsWith('data:image/jpeg;base64,') ||
|
||||
isStrictRelativeUrl(value)
|
||||
);
|
||||
}
|
||||
|
||||
export const isUnresolvedRelativeUrl = isStrictRelativeUrl;
|
||||
|
|
|
@ -48,7 +48,10 @@ validator.addFormat('strictRelativeUrl', isStrictRelativeUrl);
|
|||
validator.addFormat('unresolvedRelativeUrl', isUnresolvedRelativeUrl);
|
||||
validator.addFormat('secureUrl', isSecureUrl);
|
||||
|
||||
validator.addFormat('imageDataOrStrictRelativeUrl', imageDataOrStrictRelativeUrl);
|
||||
validator.addFormat(
|
||||
'imageDataOrStrictRelativeUrl',
|
||||
imageDataOrStrictRelativeUrl
|
||||
);
|
||||
|
||||
function filterErrors(errors) {
|
||||
if (errors) {
|
||||
|
@ -89,7 +92,7 @@ const _validateStaticTheme = validator.compile({
|
|||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
),
|
||||
id: 'static-theme-manifest',
|
||||
$ref: '#/types/ThemeManifest',
|
||||
|
@ -106,19 +109,17 @@ export const validateStaticTheme = (...args) => {
|
|||
// just need to reference WebExtensionLangpackManifest and merge it with the
|
||||
// object that has additionalProperties: false.
|
||||
const _validateLangPack = validator.compile({
|
||||
...merge(
|
||||
schemaObject, {
|
||||
types: {
|
||||
WebExtensionLangpackManifest: {
|
||||
$merge: {
|
||||
with: {
|
||||
additionalProperties: false,
|
||||
},
|
||||
...merge(schemaObject, {
|
||||
types: {
|
||||
WebExtensionLangpackManifest: {
|
||||
$merge: {
|
||||
with: {
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
},
|
||||
}),
|
||||
id: 'langpack-manifest',
|
||||
$ref: '#/types/WebExtensionLangpackManifest',
|
||||
});
|
||||
|
@ -132,19 +133,17 @@ export const validateLangPack = (...args) => {
|
|||
// Like with langpacks, we don't want additional properties in dictionaries,
|
||||
// and there is no separate schema file.
|
||||
const _validateDictionary = validator.compile({
|
||||
...merge(
|
||||
schemaObject, {
|
||||
types: {
|
||||
WebExtensionDictionaryManifest: {
|
||||
$merge: {
|
||||
with: {
|
||||
additionalProperties: false,
|
||||
},
|
||||
...merge(schemaObject, {
|
||||
types: {
|
||||
WebExtensionDictionaryManifest: {
|
||||
$merge: {
|
||||
with: {
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
},
|
||||
}),
|
||||
id: 'dictionary-manifest',
|
||||
$ref: '#/types/WebExtensionDictionaryManifest',
|
||||
});
|
||||
|
|
62
src/utils.js
62
src/utils.js
|
@ -12,10 +12,8 @@ import { PACKAGE_TYPES, LOCAL_PROTOCOLS } from 'const';
|
|||
|
||||
/* global nodeRequire, localesRoot */
|
||||
|
||||
|
||||
const SOURCE_MAP_RE = new RegExp(/\/\/[#@]\s(source(?:Mapping)?URL)=\s*(\S+)/);
|
||||
|
||||
|
||||
export function normalizePath(iconPath) {
|
||||
// Convert the icon path to a URL so we can strip any fragments and resolve
|
||||
// . and .. automatically. We need an absolute URL to use as a base so we're
|
||||
|
@ -75,22 +73,31 @@ export function getNodeReference(context, node) {
|
|||
}
|
||||
}
|
||||
|
||||
if (scopeVar && scopeVar.defs && scopeVar.defs[0] &&
|
||||
scopeVar.defs[0].parent && scopeVar.defs[0].parent.parent &&
|
||||
scopeVar.defs[0].parent.parent.body) {
|
||||
if (
|
||||
scopeVar &&
|
||||
scopeVar.defs &&
|
||||
scopeVar.defs[0] &&
|
||||
scopeVar.defs[0].parent &&
|
||||
scopeVar.defs[0].parent.parent &&
|
||||
scopeVar.defs[0].parent.parent.body
|
||||
) {
|
||||
// This represents all occurrences of the variable
|
||||
const occurances = scopeVar.defs[0].parent.parent.body;
|
||||
let lastAssignment;
|
||||
|
||||
if (occurances instanceof Array) {
|
||||
occurances.forEach((occurance) => {
|
||||
if (occurance.type === 'VariableDeclaration' &&
|
||||
occurance.declarations[0].init !== null) {
|
||||
if (
|
||||
occurance.type === 'VariableDeclaration' &&
|
||||
occurance.declarations[0].init !== null
|
||||
) {
|
||||
// Get what the name of what it was assigned to or the raw
|
||||
// value depending on the initalization
|
||||
lastAssignment = occurance.declarations[0].init;
|
||||
} else if (occurance.type === 'ExpressionStatement' &&
|
||||
occurance.expression.type === 'AssignmentExpression') {
|
||||
} else if (
|
||||
occurance.type === 'ExpressionStatement' &&
|
||||
occurance.expression.type === 'AssignmentExpression'
|
||||
) {
|
||||
// Get the right hand side of the assignment
|
||||
lastAssignment = occurance.expression.right;
|
||||
}
|
||||
|
@ -117,8 +124,13 @@ export function getVariable(context, name) {
|
|||
const { variables } = context.getScope();
|
||||
let result;
|
||||
variables.forEach((variable) => {
|
||||
if (variable.name === name && variable.defs && variable.defs[0] &&
|
||||
variable.defs[0].name && variable.defs[0].name.parent) {
|
||||
if (
|
||||
variable.name === name &&
|
||||
variable.defs &&
|
||||
variable.defs[0] &&
|
||||
variable.defs[0].name &&
|
||||
variable.defs[0].name.parent
|
||||
) {
|
||||
result = variable.defs[0].name.parent.init;
|
||||
}
|
||||
});
|
||||
|
@ -150,9 +162,15 @@ export function buildI18nObject(i18nData) {
|
|||
return {
|
||||
jed: _jed,
|
||||
getI18Data,
|
||||
_: (str) => { return _jed.gettext(str); },
|
||||
gettext: (str) => { return _jed.gettext(str); },
|
||||
sprintf: (fmt, args) => { return _jed.sprintf(fmt, args); },
|
||||
_: (str) => {
|
||||
return _jed.gettext(str);
|
||||
},
|
||||
gettext: (str) => {
|
||||
return _jed.gettext(str);
|
||||
},
|
||||
sprintf: (fmt, args) => {
|
||||
return _jed.sprintf(fmt, args);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -196,8 +214,10 @@ export function ignorePrivateFunctions(list) {
|
|||
const filteredList = {};
|
||||
|
||||
Object.keys(list).forEach((functionName) => {
|
||||
if (functionName.startsWith('_') === false &&
|
||||
typeof list[functionName] === 'function') {
|
||||
if (
|
||||
functionName.startsWith('_') === false &&
|
||||
typeof list[functionName] === 'function'
|
||||
) {
|
||||
filteredList[functionName] = list[functionName];
|
||||
}
|
||||
});
|
||||
|
@ -205,7 +225,6 @@ export function ignorePrivateFunctions(list) {
|
|||
return filteredList;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Check a filename to make sure it's valid; used by scanners so we never
|
||||
* accept new scanners that don't specify which file they're referencing.
|
||||
|
@ -216,7 +235,6 @@ export function ensureFilenameExists(filename) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
export function isLocalUrl(urlInput) {
|
||||
const parsedUrl = url.parse(urlInput);
|
||||
const { protocol, path } = parsedUrl;
|
||||
|
@ -272,18 +290,14 @@ export function parseCspPolicy(policy) {
|
|||
return parsedPolicy;
|
||||
}
|
||||
|
||||
|
||||
export function getLineAndColumnFromMatch(match) {
|
||||
const matchedLines = match.input
|
||||
.substr(0, match.index)
|
||||
.split('\n');
|
||||
const matchedLines = match.input.substr(0, match.index).split('\n');
|
||||
const matchedColumn = matchedLines.slice(-1)[0].length + 1;
|
||||
const matchedLine = matchedLines.length;
|
||||
|
||||
return { matchedLine, matchedColumn };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determines if the source text is minified.
|
||||
* Using the percentage no. of the indented lines from a sample set of lines
|
||||
|
@ -347,7 +361,7 @@ export function couldBeMinifiedCode(code) {
|
|||
}
|
||||
|
||||
return (
|
||||
((indentCount / lines) * 100) < indentCountThreshold ||
|
||||
(indentCount / lines) * 100 < indentCountThreshold ||
|
||||
hugeLinesCount > hugeLinesThreshold
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ describe('Basic integration test', () => {
|
|||
it('linter should fail if ran without params with explanation', async () => {
|
||||
const { exitCode, stderr } = await executeScript('addons-linter');
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stderr).toContain('Not enough non-option arguments: got 0, need at least 1');
|
||||
expect(stderr).toContain(
|
||||
'Not enough non-option arguments: got 0, need at least 1'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,8 +3,9 @@ import { exec } from 'child_process';
|
|||
|
||||
// Allow to force the scripts bin paths using the TEST_BIN_PATH environment var,
|
||||
// used on Travis to run the tests on a production-like addons-linter package.
|
||||
export const BIN_PATH = process.env.TEST_BIN_PATH ?
|
||||
process.env.TEST_BIN_PATH : path.join(__dirname, '../../bin/');
|
||||
export const BIN_PATH = process.env.TEST_BIN_PATH
|
||||
? process.env.TEST_BIN_PATH
|
||||
: path.join(__dirname, '../../bin/');
|
||||
|
||||
export function executeScript(scriptName, args = [], options = {}) {
|
||||
const scriptPath = path.join(BIN_PATH, scriptName);
|
||||
|
@ -12,15 +13,19 @@ export function executeScript(scriptName, args = [], options = {}) {
|
|||
const execOptions = { shell: true, ...options };
|
||||
|
||||
return new Promise((resolve) => {
|
||||
exec(`node ${scriptPath} ${args.join(' ')}`, execOptions, (error, stdout, stderr) => {
|
||||
const exitCode = error ? error.code : 0;
|
||||
exec(
|
||||
`node ${scriptPath} ${args.join(' ')}`,
|
||||
execOptions,
|
||||
(error, stdout, stderr) => {
|
||||
const exitCode = error ? error.code : 0;
|
||||
|
||||
resolve({
|
||||
exitCode,
|
||||
error,
|
||||
stderr,
|
||||
stdout,
|
||||
});
|
||||
});
|
||||
resolve({
|
||||
exitCode,
|
||||
error,
|
||||
stderr,
|
||||
stdout,
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -48,15 +48,19 @@ function getPackedName() {
|
|||
function createPackage(tmpDirPath) {
|
||||
console.log(chalk.green('Create a pre-release npm package archive'));
|
||||
return new Promise((resolve, reject) => {
|
||||
const pkgPack = spawnWithShell('npm', ['pack', process.cwd()], { cwd: tmpDirPath });
|
||||
const pkgPack = spawnWithShell('npm', ['pack', process.cwd()], {
|
||||
cwd: tmpDirPath,
|
||||
});
|
||||
|
||||
pkgPack.stdout.pipe(process.stdout);
|
||||
pkgPack.stderr.pipe(process.stderr);
|
||||
pkgPack.on('close', (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
resolve(getPackedName().then((filename) => {
|
||||
return path.join(tmpDirPath, filename);
|
||||
}));
|
||||
resolve(
|
||||
getPackedName().then((filename) => {
|
||||
return path.join(tmpDirPath, filename);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
reject(new Error('Failed to create npm package archive'));
|
||||
}
|
||||
|
@ -65,7 +69,9 @@ function createPackage(tmpDirPath) {
|
|||
}
|
||||
|
||||
function unpackTarPackage(packagePath, destDir) {
|
||||
console.log(chalk.green(['Unpacking', packagePath, 'package into', destDir].join(' ')));
|
||||
console.log(
|
||||
chalk.green(['Unpacking', packagePath, 'package into', destDir].join(' '))
|
||||
);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.createReadStream(packagePath)
|
||||
|
@ -79,9 +85,13 @@ function unpackTarPackage(packagePath, destDir) {
|
|||
function installPackageDeps(packageDir) {
|
||||
console.log(chalk.green('Install production package dependencies'));
|
||||
return new Promise((resolve, reject) => {
|
||||
const pkgInstall = spawnWithShell('npm', ['install', '--production', '--no-lockfile'], {
|
||||
cwd: packageDir,
|
||||
});
|
||||
const pkgInstall = spawnWithShell(
|
||||
'npm',
|
||||
['install', '--production', '--no-lockfile'],
|
||||
{
|
||||
cwd: packageDir,
|
||||
}
|
||||
);
|
||||
pkgInstall.stdout.pipe(process.stdout);
|
||||
pkgInstall.stderr.pipe(process.stderr);
|
||||
pkgInstall.on('close', (exitCode) => {
|
||||
|
@ -95,14 +105,20 @@ function installPackageDeps(packageDir) {
|
|||
}
|
||||
|
||||
function runIntegrationTests(packageDir) {
|
||||
console.log(chalk.green('Running integration tests in production-like environent'));
|
||||
console.log(
|
||||
chalk.green('Running integration tests in production-like environent')
|
||||
);
|
||||
return new Promise((resolve, reject) => {
|
||||
const testRun = spawnWithShell('npm', ['run', npmScript, '--', jestTestsPath], {
|
||||
env: Object.assign({}, process.env, {
|
||||
PATH: process.env.PATH,
|
||||
TEST_BIN_PATH: path.join(packageDir, 'bin'),
|
||||
}),
|
||||
});
|
||||
const testRun = spawnWithShell(
|
||||
'npm',
|
||||
['run', npmScript, '--', jestTestsPath],
|
||||
{
|
||||
env: Object.assign({}, process.env, {
|
||||
PATH: process.env.PATH,
|
||||
TEST_BIN_PATH: path.join(packageDir, 'bin'),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
testRun.stdout.pipe(process.stdout);
|
||||
testRun.stderr.pipe(process.stderr);
|
||||
|
@ -118,15 +134,17 @@ function runIntegrationTests(packageDir) {
|
|||
|
||||
// Create a production-like environment in a temporarily created directory
|
||||
// and then run the integration tests on it.
|
||||
tmp.withDir((tmpDir) => {
|
||||
const tmpDirPath = tmpDir.path;
|
||||
const unpackedDirPath = path.join(tmpDirPath, 'package');
|
||||
tmp
|
||||
.withDir((tmpDir) => {
|
||||
const tmpDirPath = tmpDir.path;
|
||||
const unpackedDirPath = path.join(tmpDirPath, 'package');
|
||||
|
||||
return createPackage(tmpDirPath)
|
||||
.then((archiveFilePath) => unpackTarPackage(archiveFilePath, tmpDirPath))
|
||||
.then(() => installPackageDeps(unpackedDirPath))
|
||||
.then(() => runIntegrationTests(unpackedDirPath));
|
||||
}, tmpOptions).catch((err) => {
|
||||
console.error(err.stack ? chalk.red(err.stack) : chalk.red(err));
|
||||
process.exit(1);
|
||||
});
|
||||
return createPackage(tmpDirPath)
|
||||
.then((archiveFilePath) => unpackTarPackage(archiveFilePath, tmpDirPath))
|
||||
.then(() => installPackageDeps(unpackedDirPath))
|
||||
.then(() => runIntegrationTests(unpackedDirPath));
|
||||
}, tmpOptions)
|
||||
.catch((err) => {
|
||||
console.error(err.stack ? chalk.red(err.stack) : chalk.red(err));
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
@ -8,7 +8,6 @@ import { oneLine } from 'common-tags';
|
|||
|
||||
import { PACKAGE_EXTENSION } from 'const';
|
||||
|
||||
|
||||
export const fakeMessageData = {
|
||||
code: 'WHATEVER_CODE',
|
||||
description: 'description',
|
||||
|
@ -18,7 +17,8 @@ export const fakeMessageData = {
|
|||
export const EMPTY_PNG = Buffer.from(
|
||||
oneLine`iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMA
|
||||
AQAABQABDQottAAAAABJRU5ErkJggg==`,
|
||||
'base64');
|
||||
'base64'
|
||||
);
|
||||
|
||||
export function getRuleFiles(ruleType) {
|
||||
const ruleFiles = fs.readdirSync(`src/rules/${ruleType}`);
|
||||
|
@ -48,7 +48,11 @@ export function getVariable(scope, name) {
|
|||
return variable;
|
||||
}
|
||||
|
||||
export function metadataPassCheck(contents, filename, { addonMetadata = null } = {}) {
|
||||
export function metadataPassCheck(
|
||||
contents,
|
||||
filename,
|
||||
{ addonMetadata = null } = {}
|
||||
) {
|
||||
if (!addonMetadata || typeof addonMetadata.guid === 'undefined') {
|
||||
assert.fail(null, null, 'Add-on metadata not found');
|
||||
}
|
||||
|
@ -70,89 +74,122 @@ export function validHTML(contents = '') {
|
|||
}
|
||||
|
||||
export function validMetadata(metadata = {}) {
|
||||
return Object.assign({}, {
|
||||
type: PACKAGE_EXTENSION,
|
||||
}, metadata);
|
||||
return Object.assign(
|
||||
{},
|
||||
{
|
||||
type: PACKAGE_EXTENSION,
|
||||
},
|
||||
metadata
|
||||
);
|
||||
}
|
||||
|
||||
export function validManifestJSON(extra) {
|
||||
return JSON.stringify(Object.assign({}, {
|
||||
name: 'my extension',
|
||||
manifest_version: 2,
|
||||
applications: {
|
||||
gecko: {
|
||||
id: '{daf44bf7-a45e-4450-979c-91cf07434c3d}',
|
||||
strict_min_version: '40.0.0',
|
||||
return JSON.stringify(
|
||||
Object.assign(
|
||||
{},
|
||||
{
|
||||
name: 'my extension',
|
||||
manifest_version: 2,
|
||||
applications: {
|
||||
gecko: {
|
||||
id: '{daf44bf7-a45e-4450-979c-91cf07434c3d}',
|
||||
strict_min_version: '40.0.0',
|
||||
},
|
||||
},
|
||||
version: '0.1',
|
||||
},
|
||||
},
|
||||
version: '0.1',
|
||||
}, extra));
|
||||
extra
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function validDictionaryManifestJSON(extra) {
|
||||
return JSON.stringify(Object.assign({}, {
|
||||
manifest_version: 2,
|
||||
name: 'My French Dictionary',
|
||||
version: '57.0a1',
|
||||
dictionaries: {
|
||||
fr: '/path/to/fr.dic',
|
||||
},
|
||||
}, extra));
|
||||
return JSON.stringify(
|
||||
Object.assign(
|
||||
{},
|
||||
{
|
||||
manifest_version: 2,
|
||||
name: 'My French Dictionary',
|
||||
version: '57.0a1',
|
||||
dictionaries: {
|
||||
fr: '/path/to/fr.dic',
|
||||
},
|
||||
},
|
||||
extra
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function validLangpackManifestJSON(extra) {
|
||||
return JSON.stringify(Object.assign({}, {
|
||||
manifest_version: 2,
|
||||
name: 'My Language Pack',
|
||||
version: '57.0',
|
||||
langpack_id: 'de',
|
||||
languages: {
|
||||
de: {
|
||||
chrome_resources: {},
|
||||
version: '57.0a1',
|
||||
return JSON.stringify(
|
||||
Object.assign(
|
||||
{},
|
||||
{
|
||||
manifest_version: 2,
|
||||
name: 'My Language Pack',
|
||||
version: '57.0',
|
||||
langpack_id: 'de',
|
||||
languages: {
|
||||
de: {
|
||||
chrome_resources: {},
|
||||
version: '57.0a1',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, extra));
|
||||
extra
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function validStaticThemeManifestJSON(extra) {
|
||||
return JSON.stringify(Object.assign({}, {
|
||||
manifest_version: 2,
|
||||
name: 'My Static Theme',
|
||||
version: '1.0',
|
||||
theme: {
|
||||
images: {
|
||||
headerURL: 'weta.png',
|
||||
return JSON.stringify(
|
||||
Object.assign(
|
||||
{},
|
||||
{
|
||||
manifest_version: 2,
|
||||
name: 'My Static Theme',
|
||||
version: '1.0',
|
||||
theme: {
|
||||
images: {
|
||||
headerURL: 'weta.png',
|
||||
},
|
||||
colors: {
|
||||
accentcolor: '#adb09f',
|
||||
textcolor: '#000',
|
||||
background_tab_text: 'rgba(255, 192, 0, 0)',
|
||||
toolbar_text: 'rgb(255, 255, 255),',
|
||||
toolbar_field_text: 'hsl(120, 100%, 50%)',
|
||||
},
|
||||
},
|
||||
},
|
||||
colors: {
|
||||
accentcolor: '#adb09f',
|
||||
textcolor: '#000',
|
||||
background_tab_text: 'rgba(255, 192, 0, 0)',
|
||||
toolbar_text: 'rgb(255, 255, 255),',
|
||||
toolbar_field_text: 'hsl(120, 100%, 50%)',
|
||||
},
|
||||
},
|
||||
}, extra));
|
||||
extra
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function validLocaleMessagesJSON() {
|
||||
return JSON.stringify(Object.assign({}, {
|
||||
foo: {
|
||||
message: 'bar',
|
||||
},
|
||||
Placeh0lder_Test: {
|
||||
message: '$foo$ bar $BA2$',
|
||||
placeholders: {
|
||||
return JSON.stringify(
|
||||
Object.assign(
|
||||
{},
|
||||
{
|
||||
foo: {
|
||||
content: '$1',
|
||||
example: 'FOO',
|
||||
message: 'bar',
|
||||
},
|
||||
BA2: {
|
||||
content: 'baz',
|
||||
Placeh0lder_Test: {
|
||||
message: '$foo$ bar $BA2$',
|
||||
placeholders: {
|
||||
foo: {
|
||||
content: '$1',
|
||||
example: 'FOO',
|
||||
},
|
||||
BA2: {
|
||||
content: 'baz',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function unexpectedSuccess() {
|
||||
|
@ -182,9 +219,11 @@ export function getStreamableIO(files) {
|
|||
return Promise.resolve(files);
|
||||
},
|
||||
getFileAsStream: (path) => {
|
||||
if (files[path] instanceof Stream &&
|
||||
typeof files[path]._read === 'function' &&
|
||||
typeof files[path]._readableState === 'object') {
|
||||
if (
|
||||
files[path] instanceof Stream &&
|
||||
typeof files[path]._read === 'function' &&
|
||||
typeof files[path]._readableState === 'object'
|
||||
) {
|
||||
return Promise.resolve(files[path]);
|
||||
}
|
||||
|
||||
|
@ -209,7 +248,9 @@ export function checkOutput(func, argv, callback) {
|
|||
var _log = console.log; // eslint-disable-line
|
||||
var _warn = console.warn; // eslint-disable-line
|
||||
|
||||
process.exit = function(code) { exitCode = code; };
|
||||
process.exit = function(code) {
|
||||
exitCode = code;
|
||||
};
|
||||
process.env = Hash.merge(process.env, { _: 'node' });
|
||||
process.argv = argv || ['./usage'];
|
||||
|
||||
|
@ -217,9 +258,15 @@ export function checkOutput(func, argv, callback) {
|
|||
const logs = [];
|
||||
const warnings = [];
|
||||
|
||||
console.error = function(msg) { errors.push(msg); }; // eslint-disable-line
|
||||
console.log = function(msg) { logs.push(msg); }; // eslint-disable-line
|
||||
console.warn = function(msg) { warnings.push(msg); }; // eslint-disable-line
|
||||
console.error = function(msg) {
|
||||
errors.push(msg);
|
||||
}; // eslint-disable-line
|
||||
console.log = function(msg) {
|
||||
logs.push(msg);
|
||||
}; // eslint-disable-line
|
||||
console.warn = function(msg) {
|
||||
warnings.push(msg);
|
||||
}; // eslint-disable-line
|
||||
|
||||
let result;
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ import { FLAGGED_FILE_MAGIC_NUMBERS_LENGTH } from 'const';
|
|||
|
||||
import { unexpectedSuccess } from '../helpers';
|
||||
|
||||
|
||||
describe('io.IOBase()', () => {
|
||||
it('should init class props as expected', () => {
|
||||
const io = new IOBase('foo/bar');
|
||||
|
@ -80,8 +79,12 @@ describe('io.IOBase()', () => {
|
|||
const io = new IOBase('foo/bar');
|
||||
io.getChunkAsBuffer = sinon.stub();
|
||||
io.getFile('get-a-chunk-as-buffer', 'chunk');
|
||||
expect(io.getChunkAsBuffer.calledWith('get-a-chunk-as-buffer',
|
||||
FLAGGED_FILE_MAGIC_NUMBERS_LENGTH)).toBeTruthy();
|
||||
expect(
|
||||
io.getChunkAsBuffer.calledWith(
|
||||
'get-a-chunk-as-buffer',
|
||||
FLAGGED_FILE_MAGIC_NUMBERS_LENGTH
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should scan all files by default', () => {
|
||||
|
|
|
@ -5,7 +5,6 @@ import { DEFLATE_COMPRESSION, NO_COMPRESSION } from 'const';
|
|||
|
||||
import { unexpectedSuccess } from '../helpers';
|
||||
|
||||
|
||||
const defaultData = {
|
||||
compressionMethod: DEFLATE_COMPRESSION,
|
||||
};
|
||||
|
@ -59,7 +58,6 @@ describe('Crx.open()', function openCallback() {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
describe('crx.getFiles()', function getFilesCallback() {
|
||||
beforeEach(() => {
|
||||
const onStub = sinon.stub();
|
||||
|
@ -102,8 +100,12 @@ describe('crx.getFiles()', function getFilesCallback() {
|
|||
});
|
||||
|
||||
it('should contain expected files', async () => {
|
||||
const myCrx = new Crx('foo/bar', this.fakeZipLib, this.fakeParseCrx,
|
||||
this.fakeFs);
|
||||
const myCrx = new Crx(
|
||||
'foo/bar',
|
||||
this.fakeZipLib,
|
||||
this.fakeParseCrx,
|
||||
this.fakeFs
|
||||
);
|
||||
const expected = {
|
||||
'chrome.manifest': chromeManifestEntry,
|
||||
};
|
||||
|
@ -133,8 +135,12 @@ describe('crx.getFiles()', function getFilesCallback() {
|
|||
});
|
||||
|
||||
it('should reject on duplicate entries', async () => {
|
||||
const myCrx = new Crx('foo/bar', this.fakeZipLib, this.fakeParseCrx,
|
||||
this.fakeFs);
|
||||
const myCrx = new Crx(
|
||||
'foo/bar',
|
||||
this.fakeZipLib,
|
||||
this.fakeParseCrx,
|
||||
this.fakeFs
|
||||
);
|
||||
this.fromBufferStub.yieldsAsync(null, this.fakeZipFile);
|
||||
this.fakeParseCrx.yieldsAsync(null, { body: Buffer.from('foo') });
|
||||
this.readFileStub.yieldsAsync(null, Buffer.from('bar'));
|
||||
|
@ -155,8 +161,12 @@ describe('crx.getFiles()', function getFilesCallback() {
|
|||
});
|
||||
|
||||
it('should reject on errors in readFile() in open()', async () => {
|
||||
const myCrx = new Crx('foo/bar', this.fakeZipLib, this.fakeParseCrx,
|
||||
this.fakeFs);
|
||||
const myCrx = new Crx(
|
||||
'foo/bar',
|
||||
this.fakeZipLib,
|
||||
this.fakeParseCrx,
|
||||
this.fakeFs
|
||||
);
|
||||
|
||||
this.readFileStub.yieldsAsync(new Error('open test'), Buffer.from('bar'));
|
||||
|
||||
|
@ -169,8 +179,12 @@ describe('crx.getFiles()', function getFilesCallback() {
|
|||
});
|
||||
|
||||
it('should reject on errors in parseCRX() in open()', async () => {
|
||||
const myCrx = new Crx('foo/bar', this.fakeZipLib, this.fakeParseCrx,
|
||||
this.fakeFs);
|
||||
const myCrx = new Crx(
|
||||
'foo/bar',
|
||||
this.fakeZipLib,
|
||||
this.fakeParseCrx,
|
||||
this.fakeFs
|
||||
);
|
||||
|
||||
this.readFileStub.yieldsAsync(null, Buffer.from('bar'));
|
||||
this.fakeParseCrx.yieldsAsync(new Error('open test'), null);
|
||||
|
@ -184,8 +198,12 @@ describe('crx.getFiles()', function getFilesCallback() {
|
|||
});
|
||||
|
||||
it('should reject on errors in fromBuffer() in open()', async () => {
|
||||
const myCrx = new Crx('foo/bar', this.fakeZipLib, this.fakeParseCrx,
|
||||
this.fakeFs);
|
||||
const myCrx = new Crx(
|
||||
'foo/bar',
|
||||
this.fakeZipLib,
|
||||
this.fakeParseCrx,
|
||||
this.fakeFs
|
||||
);
|
||||
|
||||
this.fromBufferStub.yieldsAsync(new Error('open test'), this.fakeZipFile);
|
||||
this.fakeParseCrx.yieldsAsync(null, { body: Buffer.from('foo') });
|
||||
|
|
|
@ -4,7 +4,6 @@ import { Directory } from 'io';
|
|||
|
||||
import { readStringFromStream } from '../helpers';
|
||||
|
||||
|
||||
describe('Directory.getFiles()', () => {
|
||||
it('should return cached data when available', async () => {
|
||||
const myDirectory = new Directory('tests/fixtures/io/');
|
||||
|
@ -68,9 +67,9 @@ describe('Directory._getPath()', () => {
|
|||
const myDirectory = new Directory('tests/fixtures/io/');
|
||||
|
||||
await myDirectory.getFiles();
|
||||
await expect(
|
||||
myDirectory.getPath('whatever')
|
||||
).rejects.toThrow('"whatever" does not exist in this dir.');
|
||||
await expect(myDirectory.getPath('whatever')).rejects.toThrow(
|
||||
'"whatever" does not exist in this dir.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject if path does not start with base', async () => {
|
||||
|
@ -79,9 +78,9 @@ describe('Directory._getPath()', () => {
|
|||
'../file1.txt': {},
|
||||
};
|
||||
|
||||
await expect(
|
||||
myDirectory.getPath('../file1.txt')
|
||||
).rejects.toThrow('Path argument must be relative');
|
||||
await expect(myDirectory.getPath('../file1.txt')).rejects.toThrow(
|
||||
'Path argument must be relative'
|
||||
);
|
||||
});
|
||||
|
||||
it("should reject if path starts with '/'", async () => {
|
||||
|
@ -90,9 +89,9 @@ describe('Directory._getPath()', () => {
|
|||
'/file1.txt': {},
|
||||
};
|
||||
|
||||
await expect(
|
||||
myDirectory.getPath('/file1.txt')
|
||||
).rejects.toThrow('Path argument must be relative');
|
||||
await expect(myDirectory.getPath('/file1.txt')).rejects.toThrow(
|
||||
'Path argument must be relative'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -103,24 +102,32 @@ describe('Directory.getFileAsStream()', () => {
|
|||
|
||||
const readStream = await myDirectory.getFileAsStream('dir2/dir3/file3.txt');
|
||||
|
||||
await expect(
|
||||
readStringFromStream(readStream)
|
||||
).resolves.toBe('123\n');
|
||||
await expect(readStringFromStream(readStream)).resolves.toBe('123\n');
|
||||
});
|
||||
|
||||
it('should not enforce utf-8 when encoding = null', async () => {
|
||||
const myDirectory = new Directory('tests/fixtures/io/');
|
||||
await myDirectory.getFiles();
|
||||
|
||||
const readStreamEncodingDefault = await myDirectory.getFileAsStream('dir2/dir3/file.png');
|
||||
const readStreamEncodingDefault = await myDirectory.getFileAsStream(
|
||||
'dir2/dir3/file.png'
|
||||
);
|
||||
|
||||
const readStreamEncodingNull = await myDirectory.getFileAsStream(
|
||||
'dir2/dir3/file.png', {
|
||||
'dir2/dir3/file.png',
|
||||
{
|
||||
encoding: null,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const stringFromEncodingDefault = await readStringFromStream(readStreamEncodingDefault, 'binary');
|
||||
const stringFromEncodingNull = await readStringFromStream(readStreamEncodingNull, 'binary');
|
||||
const stringFromEncodingDefault = await readStringFromStream(
|
||||
readStreamEncodingDefault,
|
||||
'binary'
|
||||
);
|
||||
const stringFromEncodingNull = await readStringFromStream(
|
||||
readStreamEncodingNull,
|
||||
'binary'
|
||||
);
|
||||
|
||||
// Ensure that by setting the encoding to null, the utf-8 encoding is not enforced
|
||||
// while reading binary data from the stream.
|
||||
|
@ -128,7 +135,9 @@ describe('Directory.getFileAsStream()', () => {
|
|||
|
||||
// Confirms that the default "utf-8" encoding behavior is still preserved when the encoding
|
||||
// is not been explicitly specified.
|
||||
expect(stringFromEncodingDefault.slice(0, 8)).not.toEqual('\x89PNG\r\n\x1a\n');
|
||||
expect(stringFromEncodingDefault.slice(0, 8)).not.toEqual(
|
||||
'\x89PNG\r\n\x1a\n'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject if file is too big', async () => {
|
||||
|
@ -141,20 +150,19 @@ describe('Directory.getFileAsStream()', () => {
|
|||
'chrome.manifest': fakeFileMeta,
|
||||
};
|
||||
|
||||
await expect(
|
||||
myDirectory.getFileAsStream('manifest.json')
|
||||
).rejects.toThrow('File "manifest.json" is too large');
|
||||
await expect(myDirectory.getFileAsStream('manifest.json')).rejects.toThrow(
|
||||
'File "manifest.json" is too large'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Directory.getFileAsString()', () => {
|
||||
it('should strip a BOM', async () => {
|
||||
const myDirectory = new Directory('tests/fixtures/io/');
|
||||
|
||||
await myDirectory.getFiles();
|
||||
const content = await myDirectory.getFileAsString('dir3/foo.txt');
|
||||
expect(content.charCodeAt(0)).not.toEqual(0xFEFF);
|
||||
expect(content.charCodeAt(0)).not.toEqual(0xfeff);
|
||||
});
|
||||
|
||||
it('should return a string', async () => {
|
||||
|
@ -182,9 +190,9 @@ describe('Directory.getFileAsString()', () => {
|
|||
return Promise.resolve(fakeStreamEmitter);
|
||||
};
|
||||
|
||||
await expect(
|
||||
myDirectory.getFileAsString('manifest.json')
|
||||
).rejects.toThrow('¡hola!');
|
||||
await expect(myDirectory.getFileAsString('manifest.json')).rejects.toThrow(
|
||||
'¡hola!'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject if file is too big', async () => {
|
||||
|
@ -197,9 +205,9 @@ describe('Directory.getFileAsString()', () => {
|
|||
'chrome.manifest': fakeFileMeta,
|
||||
};
|
||||
|
||||
await expect(
|
||||
myDirectory.getFileAsString('manifest.json')
|
||||
).rejects.toThrow('File "manifest.json" is too large');
|
||||
await expect(myDirectory.getFileAsString('manifest.json')).rejects.toThrow(
|
||||
'File "manifest.json" is too large'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -17,12 +17,11 @@ describe('io.utils.walkPromise()', () => {
|
|||
});
|
||||
|
||||
it('can be configured to not walk a directory', async () => {
|
||||
const files = await walkPromise(
|
||||
'tests/fixtures/io/', {
|
||||
shouldIncludePath: (filePath) => {
|
||||
return !filePath.startsWith('dir2');
|
||||
},
|
||||
});
|
||||
const files = await walkPromise('tests/fixtures/io/', {
|
||||
shouldIncludePath: (filePath) => {
|
||||
return !filePath.startsWith('dir2');
|
||||
},
|
||||
});
|
||||
const fileNames = Object.keys(files);
|
||||
expect(fileNames).toContain('dir1/file1.txt');
|
||||
expect(fileNames).not.toContain('dir2/file2.txt');
|
||||
|
@ -30,25 +29,23 @@ describe('io.utils.walkPromise()', () => {
|
|||
});
|
||||
|
||||
it('can be configured to not include a file', async () => {
|
||||
const files = await walkPromise(
|
||||
'tests/fixtures/io/', {
|
||||
shouldIncludePath: (filePath) => {
|
||||
return filePath !== 'dir2/file2.txt';
|
||||
},
|
||||
});
|
||||
const files = await walkPromise('tests/fixtures/io/', {
|
||||
shouldIncludePath: (filePath) => {
|
||||
return filePath !== 'dir2/file2.txt';
|
||||
},
|
||||
});
|
||||
const fileNames = Object.keys(files);
|
||||
expect(fileNames).not.toContain('dir2/file2.txt');
|
||||
expect(fileNames).toContain('dir2/dir3/file3.txt');
|
||||
});
|
||||
|
||||
it('can exclude the topmost directory', async () => {
|
||||
const files = await walkPromise(
|
||||
'tests/fixtures/io/', {
|
||||
shouldIncludePath: (filePath) => {
|
||||
// This would be the topmost directory.
|
||||
return filePath !== '';
|
||||
},
|
||||
});
|
||||
const files = await walkPromise('tests/fixtures/io/', {
|
||||
shouldIncludePath: (filePath) => {
|
||||
// This would be the topmost directory.
|
||||
return filePath !== '';
|
||||
},
|
||||
});
|
||||
const fileNames = Object.keys(files);
|
||||
expect(fileNames).toEqual([]);
|
||||
});
|
||||
|
|
|
@ -7,7 +7,6 @@ import { DEFLATE_COMPRESSION, NO_COMPRESSION } from 'const';
|
|||
|
||||
import { readStringFromStream } from '../helpers';
|
||||
|
||||
|
||||
const defaultData = {
|
||||
compressionMethod: DEFLATE_COMPRESSION,
|
||||
};
|
||||
|
@ -74,23 +73,18 @@ describe('Xpi.open()', function xpiCallback() {
|
|||
// Return the fake zip to the open callback.
|
||||
this.openStub.yieldsAsync(new Error('open() test error'));
|
||||
|
||||
await expect(
|
||||
myXpi.open()
|
||||
).rejects.toThrow('open() test');
|
||||
await expect(myXpi.open()).rejects.toThrow('open() test');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('xpi.getFiles()', function getFilesCallback() {
|
||||
beforeEach(() => {
|
||||
const onStub = sinon.stub();
|
||||
// Can only yield data to the
|
||||
// callback once.
|
||||
this.entryStub = onStub
|
||||
.withArgs('entry');
|
||||
this.entryStub = onStub.withArgs('entry');
|
||||
|
||||
this.closeStub = onStub
|
||||
.withArgs('close');
|
||||
this.closeStub = onStub.withArgs('close');
|
||||
|
||||
this.fakeZipFile = {
|
||||
on: onStub,
|
||||
|
@ -115,9 +109,7 @@ describe('xpi.getFiles()', function getFilesCallback() {
|
|||
'chrome.manifest': chromeManifestEntry,
|
||||
};
|
||||
|
||||
await expect(
|
||||
myXpi.getFiles()
|
||||
).resolves.toEqual(myXpi.files);
|
||||
await expect(myXpi.getFiles()).resolves.toEqual(myXpi.files);
|
||||
expect(this.openStub.called).toBeFalsy();
|
||||
});
|
||||
|
||||
|
@ -147,9 +139,7 @@ describe('xpi.getFiles()', function getFilesCallback() {
|
|||
// Call the close event callback
|
||||
this.closeStub.yieldsAsync();
|
||||
|
||||
await expect(
|
||||
myXpi.getFiles(onEventsSubscribed)
|
||||
).resolves.toEqual(expected);
|
||||
await expect(myXpi.getFiles(onEventsSubscribed)).resolves.toEqual(expected);
|
||||
});
|
||||
|
||||
it('can be configured to exclude files', async () => {
|
||||
|
@ -213,9 +203,9 @@ describe('xpi.getFiles()', function getFilesCallback() {
|
|||
entryCallback.call(null, dupeInstallFileEntry);
|
||||
};
|
||||
|
||||
await expect(
|
||||
myXpi.getFiles(onEventsSubscribed)
|
||||
).rejects.toThrow('DuplicateZipEntry');
|
||||
await expect(myXpi.getFiles(onEventsSubscribed)).rejects.toThrow(
|
||||
'DuplicateZipEntry'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject on errors in open()', async () => {
|
||||
|
@ -223,13 +213,10 @@ describe('xpi.getFiles()', function getFilesCallback() {
|
|||
|
||||
this.openStub.yieldsAsync(new Error('open test'), this.fakeZipFile);
|
||||
|
||||
await expect(
|
||||
myXpi.getFiles()
|
||||
).rejects.toThrow('open test');
|
||||
await expect(myXpi.getFiles()).rejects.toThrow('open test');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Xpi.getFile()', function getFileCallback() {
|
||||
it('should throw if fileStreamType is incorrect', () => {
|
||||
const myXpi = new Xpi('foo/bar', this.fakeZipLib);
|
||||
|
@ -263,9 +250,9 @@ describe('Xpi.checkPath()', function checkPathCallback() {
|
|||
'chrome.manifest': chromeManifestEntry,
|
||||
};
|
||||
|
||||
await expect(
|
||||
myXpi.getFileAsStream('whatever')
|
||||
).rejects.toThrow('Path "whatever" does not exist');
|
||||
await expect(myXpi.getFileAsStream('whatever')).rejects.toThrow(
|
||||
'Path "whatever" does not exist'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject if file is too big', async () => {
|
||||
|
@ -279,9 +266,9 @@ describe('Xpi.checkPath()', function checkPathCallback() {
|
|||
'chrome.manifest': fakeFileMeta,
|
||||
};
|
||||
|
||||
await expect(
|
||||
myXpi.getFileAsStream('manifest.json')
|
||||
).rejects.toThrow('File "manifest.json" is too large');
|
||||
await expect(myXpi.getFileAsStream('manifest.json')).rejects.toThrow(
|
||||
'File "manifest.json" is too large'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject if file is too big for getFileAsString too', async () => {
|
||||
|
@ -295,9 +282,9 @@ describe('Xpi.checkPath()', function checkPathCallback() {
|
|||
'chrome.manifest': fakeFileMeta,
|
||||
};
|
||||
|
||||
await expect(
|
||||
myXpi.getFileAsString('manifest.json')
|
||||
).rejects.toThrow('File "manifest.json" is too large');
|
||||
await expect(myXpi.getFileAsString('manifest.json')).rejects.toThrow(
|
||||
'File "manifest.json" is too large'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -329,11 +316,12 @@ describe('Xpi.getChunkAsBuffer()', function getChunkAsBufferCallback() {
|
|||
|
||||
this.openStub.yieldsAsync(null, this.fakeZipFile);
|
||||
this.openReadStreamStub.yieldsAsync(
|
||||
new Error('getChunkAsBuffer openReadStream test'));
|
||||
new Error('getChunkAsBuffer openReadStream test')
|
||||
);
|
||||
|
||||
await expect(
|
||||
myXpi.getChunkAsBuffer('manifest.json')
|
||||
).rejects.toThrow('getChunkAsBuffer openReadStream test');
|
||||
await expect(myXpi.getChunkAsBuffer('manifest.json')).rejects.toThrow(
|
||||
'getChunkAsBuffer openReadStream test'
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve with a buffer', async () => {
|
||||
|
@ -358,7 +346,6 @@ describe('Xpi.getChunkAsBuffer()', function getChunkAsBufferCallback() {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Xpi.getFileAsStream()', function getFileAsStreamCallback() {
|
||||
beforeEach(() => {
|
||||
this.openReadStreamStub = sinon.stub();
|
||||
|
@ -381,11 +368,12 @@ describe('Xpi.getFileAsStream()', function getFileAsStreamCallback() {
|
|||
|
||||
this.openStub.yieldsAsync(null, this.fakeZipFile);
|
||||
this.openReadStreamStub.yieldsAsync(
|
||||
new Error('getFileAsStream openReadStream test'));
|
||||
new Error('getFileAsStream openReadStream test')
|
||||
);
|
||||
|
||||
await expect(
|
||||
myXpi.getFileAsStream('manifest.json')
|
||||
).rejects.toThrow('getFileAsStream openReadStream test');
|
||||
await expect(myXpi.getFileAsStream('manifest.json')).rejects.toThrow(
|
||||
'getFileAsStream openReadStream test'
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve with a readable stream', async () => {
|
||||
|
@ -428,9 +416,9 @@ describe('Xpi.getFileAsStream()', function getFileAsStreamCallback() {
|
|||
|
||||
this.openReadStreamStub.yields(null, rstream);
|
||||
|
||||
await expect(
|
||||
myXpi.getFileAsString('manifest.json')
|
||||
).resolves.toBe('line one\nline two');
|
||||
await expect(myXpi.getFileAsString('manifest.json')).resolves.toBe(
|
||||
'line one\nline two'
|
||||
);
|
||||
});
|
||||
|
||||
it('should strip a BOM', async () => {
|
||||
|
@ -446,7 +434,7 @@ describe('Xpi.getFileAsStream()', function getFileAsStreamCallback() {
|
|||
this.openReadStreamStub.yields(null, rstream);
|
||||
|
||||
const string = await myXpi.getFileAsString('manifest.json');
|
||||
expect(string.charCodeAt(0) === 0xFEFF).toBeFalsy();
|
||||
expect(string.charCodeAt(0) === 0xfeff).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should reject if error in openReadStream from readAsString', async () => {
|
||||
|
@ -458,11 +446,12 @@ describe('Xpi.getFileAsStream()', function getFileAsStreamCallback() {
|
|||
|
||||
this.openStub.yieldsAsync(null, this.fakeZipFile);
|
||||
this.openReadStreamStub.yields(
|
||||
new Error('getFileAsString openReadStream test'));
|
||||
new Error('getFileAsString openReadStream test')
|
||||
);
|
||||
|
||||
await expect(
|
||||
myXpi.getFileAsString('manifest.json')
|
||||
).rejects.toThrow('getFileAsString openReadStream test');
|
||||
await expect(myXpi.getFileAsString('manifest.json')).rejects.toThrow(
|
||||
'getFileAsString openReadStream test'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject if stream emits error', async () => {
|
||||
|
@ -481,9 +470,9 @@ describe('Xpi.getFileAsStream()', function getFileAsStreamCallback() {
|
|||
return Promise.resolve(fakeStreamEmitter);
|
||||
};
|
||||
|
||||
await expect(
|
||||
myXpi.getFileAsString('manifest.json')
|
||||
).rejects.toThrow('¡hola!');
|
||||
await expect(myXpi.getFileAsString('manifest.json')).rejects.toThrow(
|
||||
'¡hola!'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -550,16 +539,17 @@ describe('Xpi.getFilesByExt()', function getFilesByExtCallback() {
|
|||
expect(htmlFiles[2]).toEqual('third.html');
|
||||
|
||||
for (let i = 0; i < htmlFiles.length; i++) {
|
||||
expect(htmlFiles[i].endsWith('.html') ||
|
||||
htmlFiles[i].endsWith('.htm')).toBeTruthy();
|
||||
expect(
|
||||
htmlFiles[i].endsWith('.html') || htmlFiles[i].endsWith('.htm')
|
||||
).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it("should throw if file extension doesn't start with '.'", async () => {
|
||||
const myXpi = new Xpi('foo/bar', this.fakeZipLib);
|
||||
|
||||
await expect(
|
||||
myXpi.getFilesByExt('css')
|
||||
).rejects.toThrow('File extension must start with');
|
||||
await expect(myXpi.getFilesByExt('css')).rejects.toThrow(
|
||||
'File extension must start with'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -34,9 +34,12 @@ describe('DoctypeParser', () => {
|
|||
|
||||
it('should ignore invalid entities', () => {
|
||||
const addonLinter = new Linter({ _: ['bar'] });
|
||||
const dtdParser = new DoctypeParser(`
|
||||
const dtdParser = new DoctypeParser(
|
||||
`
|
||||
<!ENTITY "foobar">
|
||||
<!ENTITY bar.foo "barfoo">`, addonLinter.collector);
|
||||
<!ENTITY bar.foo "barfoo">`,
|
||||
addonLinter.collector
|
||||
);
|
||||
|
||||
dtdParser.parse();
|
||||
|
||||
|
@ -48,9 +51,12 @@ describe('DoctypeParser', () => {
|
|||
|
||||
it('should overwrite duplicate entities', () => {
|
||||
const addonLinter = new Linter({ _: ['bar'] });
|
||||
const dtdParser = new DoctypeParser(`
|
||||
const dtdParser = new DoctypeParser(
|
||||
`
|
||||
<!ENTITY foo 'bar'>
|
||||
<!ENTITY foo 'faz'>`, addonLinter.collector);
|
||||
<!ENTITY foo 'faz'>`,
|
||||
addonLinter.collector
|
||||
);
|
||||
|
||||
dtdParser.parse();
|
||||
|
||||
|
@ -71,7 +77,8 @@ describe('DoctypeParser', () => {
|
|||
|
||||
it('should parse excessive line breaks correctly', () => {
|
||||
const addonLinter = new Linter({ _: ['bar'] });
|
||||
const dtdParser = new DoctypeParser(`
|
||||
const dtdParser = new DoctypeParser(
|
||||
`
|
||||
<!ENTITY
|
||||
foo
|
||||
"bar">
|
||||
|
@ -93,7 +100,9 @@ describe('DoctypeParser', () => {
|
|||
'foo'>
|
||||
<!ENTITY overwrite 'bar'
|
||||
>
|
||||
`, addonLinter.collector);
|
||||
`,
|
||||
addonLinter.collector
|
||||
);
|
||||
|
||||
dtdParser.parse();
|
||||
|
||||
|
@ -111,7 +120,8 @@ describe('DoctypeParser', () => {
|
|||
|
||||
it('should ignore malformed lines but parse valid ones', () => {
|
||||
const addonLinter = new Linter({ _: ['bar'] });
|
||||
const dtdParser = new DoctypeParser(`
|
||||
const dtdParser = new DoctypeParser(
|
||||
`
|
||||
<!ENTITY foo "bar">
|
||||
<!--Malformed line should not overwrite -->
|
||||
<!ENTITY< foo "oops">
|
||||
|
@ -122,7 +132,9 @@ describe('DoctypeParser', () => {
|
|||
<!ENTITY this.is.a 'test'>
|
||||
<!ENTITY overwrite 'foo'>
|
||||
<!ENTITY overwrite 'bar'>
|
||||
`, addonLinter.collector);
|
||||
`,
|
||||
addonLinter.collector
|
||||
);
|
||||
|
||||
dtdParser.parse();
|
||||
|
||||
|
|
|
@ -6,16 +6,18 @@ import * as messages from 'messages';
|
|||
|
||||
import { assertHasMatchingError } from '../helpers';
|
||||
|
||||
|
||||
describe('FluentParser', () => {
|
||||
it('should parse valid .ftl file correctly', () => {
|
||||
const addonLinter = new Linter({ _: ['bar'] });
|
||||
const parser = new FluentParser(`
|
||||
const parser = new FluentParser(
|
||||
`
|
||||
choose-download-folder-title =
|
||||
{
|
||||
*[nominative] Foo
|
||||
[accusative] Foo2
|
||||
}`, addonLinter.collector);
|
||||
}`,
|
||||
addonLinter.collector
|
||||
);
|
||||
|
||||
parser.parse();
|
||||
|
||||
|
@ -24,7 +26,8 @@ choose-download-folder-title =
|
|||
|
||||
it('support key assignments', () => {
|
||||
const addonLinter = new Linter({ _: ['bar'] });
|
||||
const parser = new FluentParser(`
|
||||
const parser = new FluentParser(
|
||||
`
|
||||
key67
|
||||
.label = Sign In To &syncBrand.shortName.label;…
|
||||
.accesskey = Y
|
||||
|
@ -33,42 +36,47 @@ key68
|
|||
.accesskey = S
|
||||
key69
|
||||
.label = Reconnect to &syncBrand.shortName.label;…
|
||||
.accesskey = R`, addonLinter.collector);
|
||||
.accesskey = R`,
|
||||
addonLinter.collector
|
||||
);
|
||||
|
||||
parser.parse();
|
||||
|
||||
expect(parser.isValid).toEqual(true);
|
||||
expect(parser.parsedData.key67.attributes[0].value.elements[0].value).toEqual(
|
||||
'Sign In To &syncBrand.shortName.label;…'
|
||||
);
|
||||
expect(parser.parsedData.key67.attributes[1].value.elements[0].value).toEqual(
|
||||
'Y'
|
||||
);
|
||||
expect(
|
||||
parser.parsedData.key67.attributes[0].value.elements[0].value
|
||||
).toEqual('Sign In To &syncBrand.shortName.label;…');
|
||||
expect(
|
||||
parser.parsedData.key67.attributes[1].value.elements[0].value
|
||||
).toEqual('Y');
|
||||
|
||||
expect(parser.parsedData.key68.attributes[0].value.elements[0].value).toEqual(
|
||||
'Sync Now'
|
||||
);
|
||||
expect(parser.parsedData.key68.attributes[1].value.elements[0].value).toEqual(
|
||||
'S'
|
||||
);
|
||||
expect(
|
||||
parser.parsedData.key68.attributes[0].value.elements[0].value
|
||||
).toEqual('Sync Now');
|
||||
expect(
|
||||
parser.parsedData.key68.attributes[1].value.elements[0].value
|
||||
).toEqual('S');
|
||||
|
||||
expect(parser.parsedData.key69.attributes[0].value.elements[0].value).toEqual(
|
||||
'Reconnect to &syncBrand.shortName.label;…'
|
||||
);
|
||||
expect(parser.parsedData.key69.attributes[1].value.elements[0].value).toEqual(
|
||||
'R'
|
||||
);
|
||||
expect(
|
||||
parser.parsedData.key69.attributes[0].value.elements[0].value
|
||||
).toEqual('Reconnect to &syncBrand.shortName.label;…');
|
||||
expect(
|
||||
parser.parsedData.key69.attributes[1].value.elements[0].value
|
||||
).toEqual('R');
|
||||
});
|
||||
|
||||
it('supports placeable', () => {
|
||||
const addonLinter = new Linter({ _: ['bar'] });
|
||||
const parser = new FluentParser(`
|
||||
const parser = new FluentParser(
|
||||
`
|
||||
shared-photos =
|
||||
{ $user_name } { $photo_count ->
|
||||
[0] hasn't added any photos yet
|
||||
[one] added a new photo
|
||||
*[other] added { $photo_count } new photos
|
||||
}.`, addonLinter.collector);
|
||||
}.`,
|
||||
addonLinter.collector
|
||||
);
|
||||
|
||||
parser.parse();
|
||||
|
||||
|
@ -94,7 +102,8 @@ shared-photos =
|
|||
|
||||
it('supports firefox 60 beta en-gb file', () => {
|
||||
const addonLinter = new Linter({ _: ['bar'] });
|
||||
const parser = new FluentParser(`
|
||||
const parser = new FluentParser(
|
||||
`
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
@ -144,7 +153,9 @@ feature-disable-requires-restart = { -brand-short-name } must restart to disable
|
|||
should-restart-title = Restart { -brand-short-name }
|
||||
should-restart-ok = Restart { -brand-short-name } now
|
||||
revert-no-restart-button = Revert
|
||||
restart-later = Restart Later`, addonLinter.collector);
|
||||
restart-later = Restart Later`,
|
||||
addonLinter.collector
|
||||
);
|
||||
|
||||
parser.parse();
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import * as messages from 'messages';
|
|||
|
||||
import { validManifestJSON } from '../helpers';
|
||||
|
||||
|
||||
describe('JSONParser', () => {
|
||||
it('should show a message if bad JSON', () => {
|
||||
const addonLinter = new Linter({ _: ['bar'] });
|
||||
|
@ -28,18 +27,20 @@ describe('JSONParser duplicate keys', () => {
|
|||
// We aren't using oneLine here so we can test the line number
|
||||
// reporting.
|
||||
/* eslint-disable indent */
|
||||
const json = ['{',
|
||||
const json = [
|
||||
'{',
|
||||
'"description": "Very good music.",',
|
||||
'"manifest_version": 2,',
|
||||
'"name": "Prince",',
|
||||
'"version": "0.0.1",',
|
||||
'"name": "The Artist Formerly Known As Prince",',
|
||||
'"applications": {',
|
||||
'"gecko": {',
|
||||
'"id": "@webextension-guid"',
|
||||
'}',
|
||||
'"gecko": {',
|
||||
'"id": "@webextension-guid"',
|
||||
'}',
|
||||
'}'].join('\n');
|
||||
'}',
|
||||
'}',
|
||||
].join('\n');
|
||||
/* eslint-enable indent */
|
||||
|
||||
const jsonParser = new JSONParser(json, addonLinter.collector);
|
||||
|
@ -91,17 +92,19 @@ describe('JSONParser duplicate keys', () => {
|
|||
// We aren't using oneLine here so we can test the line number
|
||||
// reporting.
|
||||
/* eslint-disable indent */
|
||||
const json = ['{',
|
||||
const json = [
|
||||
'{',
|
||||
'"description": "Very good music.",',
|
||||
'"manifest_version": 2,',
|
||||
'"name": "Prince",',
|
||||
'"version": "0.0.1",',
|
||||
'"applications": {',
|
||||
'"gecko": {',
|
||||
'"id": "@webextension-guid"',
|
||||
'}',
|
||||
'"gecko": {',
|
||||
'"id": "@webextension-guid"',
|
||||
'}',
|
||||
'}'].join('\n');
|
||||
'}',
|
||||
'}',
|
||||
].join('\n');
|
||||
/* eslint-enable indent */
|
||||
const fakeRJSON = { parse: () => {} };
|
||||
const parseStub = sinon.stub(fakeRJSON, 'parse').callsFake(() => {
|
||||
|
|
|
@ -7,8 +7,10 @@ import { validLocaleMessagesJSON } from '../helpers';
|
|||
describe('LocaleMessagesJSONParser', () => {
|
||||
it('should be invalid if bad JSON', () => {
|
||||
const addonsLinter = new Linter({ _: ['bar'] });
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser('blah',
|
||||
addonsLinter.collector);
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(
|
||||
'blah',
|
||||
addonsLinter.collector
|
||||
);
|
||||
localeMessagesJSONParser.parse();
|
||||
expect(localeMessagesJSONParser.isValid).toEqual(false);
|
||||
const { errors } = addonsLinter.collector;
|
||||
|
@ -19,7 +21,8 @@ describe('LocaleMessagesJSONParser', () => {
|
|||
|
||||
it('should be invalid if placeholder has no content', () => {
|
||||
const addonsLinter = new Linter({ _: ['bar'] });
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(`{
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(
|
||||
`{
|
||||
"blah": {
|
||||
"message": "$CONTENT$",
|
||||
"placeholders": {
|
||||
|
@ -28,7 +31,9 @@ describe('LocaleMessagesJSONParser', () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
}`, addonsLinter.collector);
|
||||
}`,
|
||||
addonsLinter.collector
|
||||
);
|
||||
localeMessagesJSONParser.parse();
|
||||
expect(localeMessagesJSONParser.isValid).toEqual(false);
|
||||
const { errors } = addonsLinter.collector;
|
||||
|
@ -39,11 +44,14 @@ describe('LocaleMessagesJSONParser', () => {
|
|||
|
||||
it('should be invalid without message property for string', () => {
|
||||
const addonsLinter = new Linter({ _: ['bar'] });
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(`{
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(
|
||||
`{
|
||||
"blah": {
|
||||
"mesage": "foo"
|
||||
}
|
||||
}`, addonsLinter.collector);
|
||||
}`,
|
||||
addonsLinter.collector
|
||||
);
|
||||
localeMessagesJSONParser.parse();
|
||||
expect(localeMessagesJSONParser.isValid).toEqual(false);
|
||||
const { errors } = addonsLinter.collector;
|
||||
|
@ -54,22 +62,28 @@ describe('LocaleMessagesJSONParser', () => {
|
|||
|
||||
it('should show a warning for reserved message names', () => {
|
||||
const addonsLinter = new Linter({ _: ['bar'] });
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(`{
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(
|
||||
`{
|
||||
"@@extension_id": {
|
||||
"message": "foo"
|
||||
}
|
||||
}`, addonsLinter.collector);
|
||||
}`,
|
||||
addonsLinter.collector
|
||||
);
|
||||
localeMessagesJSONParser.parse();
|
||||
expect(localeMessagesJSONParser.isValid).toEqual(true);
|
||||
const { warnings } = addonsLinter.collector;
|
||||
expect(warnings.length).toEqual(1);
|
||||
expect(warnings[0].code).toEqual(messages.PREDEFINED_MESSAGE_NAME.code);
|
||||
expect(warnings[0].message).toEqual(messages.PREDEFINED_MESSAGE_NAME.message);
|
||||
expect(warnings[0].message).toEqual(
|
||||
messages.PREDEFINED_MESSAGE_NAME.message
|
||||
);
|
||||
});
|
||||
|
||||
it('should show warnings for missing placeholders', () => {
|
||||
const addonsLinter = new Linter({ _: ['bar'] });
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(`{
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(
|
||||
`{
|
||||
"blah": {
|
||||
"message": "foo $BAR$ $BAZ$",
|
||||
"placeholders": {
|
||||
|
@ -81,7 +95,9 @@ describe('LocaleMessagesJSONParser', () => {
|
|||
"bleh": {
|
||||
"message": "$EMPTY$"
|
||||
}
|
||||
}`, addonsLinter.collector);
|
||||
}`,
|
||||
addonsLinter.collector
|
||||
);
|
||||
localeMessagesJSONParser.parse();
|
||||
expect(localeMessagesJSONParser.isValid).toEqual(true);
|
||||
const { warnings } = addonsLinter.collector;
|
||||
|
@ -94,11 +110,14 @@ describe('LocaleMessagesJSONParser', () => {
|
|||
|
||||
it('should not be invalid on non alphanumeric message name', () => {
|
||||
const addonsLinter = new Linter({ _: ['bar'] });
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(`{
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(
|
||||
`{
|
||||
"not-valid": {
|
||||
"message": "foo"
|
||||
}
|
||||
}`, addonsLinter.collector);
|
||||
}`,
|
||||
addonsLinter.collector
|
||||
);
|
||||
localeMessagesJSONParser.parse();
|
||||
expect(localeMessagesJSONParser.isValid).toEqual(true);
|
||||
const { errors } = addonsLinter.collector;
|
||||
|
@ -107,18 +126,22 @@ describe('LocaleMessagesJSONParser', () => {
|
|||
|
||||
it('should not be invalid on empty message', () => {
|
||||
const addonsLinter = new Linter({ _: ['bar'] });
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(`{
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(
|
||||
`{
|
||||
"": {
|
||||
"message": "foo bar is the new bar foo"
|
||||
}
|
||||
}`, addonsLinter.collector);
|
||||
}`,
|
||||
addonsLinter.collector
|
||||
);
|
||||
localeMessagesJSONParser.parse();
|
||||
expect(localeMessagesJSONParser.isValid).toEqual(true);
|
||||
});
|
||||
|
||||
it('should be invalid if bad placeholder name', () => {
|
||||
const addonsLinter = new Linter({ _: ['bar'] });
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(`{
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(
|
||||
`{
|
||||
"invalid_placeholder": {
|
||||
"message": "$PH-1$",
|
||||
"placeholders": {
|
||||
|
@ -127,18 +150,23 @@ describe('LocaleMessagesJSONParser', () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
}`, addonsLinter.collector);
|
||||
}`,
|
||||
addonsLinter.collector
|
||||
);
|
||||
localeMessagesJSONParser.parse();
|
||||
expect(localeMessagesJSONParser.isValid).toEqual(false);
|
||||
const { errors } = addonsLinter.collector;
|
||||
expect(errors.length).toEqual(1);
|
||||
expect(errors[0].code).toEqual(messages.INVALID_PLACEHOLDER_NAME.code);
|
||||
expect(errors[0].message).toEqual(messages.INVALID_PLACEHOLDER_NAME.message);
|
||||
expect(errors[0].message).toEqual(
|
||||
messages.INVALID_PLACEHOLDER_NAME.message
|
||||
);
|
||||
});
|
||||
|
||||
it('should not be case sensitive for placeholder names', () => {
|
||||
const addonsLinter = new Linter({ _: ['bar'] });
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(`{
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(
|
||||
`{
|
||||
"perfectlyValidPlaceholderName": {
|
||||
"message": "$fooBarIsGreat$",
|
||||
"placeholders": {
|
||||
|
@ -147,7 +175,9 @@ describe('LocaleMessagesJSONParser', () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
}`, addonsLinter.collector);
|
||||
}`,
|
||||
addonsLinter.collector
|
||||
);
|
||||
localeMessagesJSONParser.parse();
|
||||
expect(localeMessagesJSONParser.isValid).toEqual(true);
|
||||
});
|
||||
|
@ -166,7 +196,8 @@ describe('LocaleMessagesJSONParser', () => {
|
|||
|
||||
it('should find placeholders with different casing in definition', () => {
|
||||
const addonsLinter = new Linter({ _: ['bar'] });
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(`{
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(
|
||||
`{
|
||||
"placeholder_case": {
|
||||
"message": "$fooBar$",
|
||||
"placeholders": {
|
||||
|
@ -175,7 +206,9 @@ describe('LocaleMessagesJSONParser', () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
}`, addonsLinter.collector);
|
||||
}`,
|
||||
addonsLinter.collector
|
||||
);
|
||||
localeMessagesJSONParser.parse();
|
||||
expect(localeMessagesJSONParser.isValid).toEqual(true);
|
||||
const { warnings } = addonsLinter.collector;
|
||||
|
@ -184,25 +217,31 @@ describe('LocaleMessagesJSONParser', () => {
|
|||
|
||||
it('should be invalid with case-insensitive duplicate message names', () => {
|
||||
const addonsLinter = new Linter({ _: ['bar'] });
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(`{
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(
|
||||
`{
|
||||
"duplicate": {
|
||||
"message": "foo"
|
||||
},
|
||||
"DUPLICATE": {
|
||||
"message": "bar"
|
||||
}
|
||||
}`, addonsLinter.collector);
|
||||
}`,
|
||||
addonsLinter.collector
|
||||
);
|
||||
localeMessagesJSONParser.parse();
|
||||
expect(localeMessagesJSONParser.isValid).toEqual(false);
|
||||
const { errors } = addonsLinter.collector;
|
||||
expect(errors.length).toEqual(1);
|
||||
expect(errors[0].code).toEqual(messages.JSON_DUPLICATE_KEY.code);
|
||||
expect(errors[0].description).toContain('Case-insensitive duplicate message name');
|
||||
expect(errors[0].description).toContain(
|
||||
'Case-insensitive duplicate message name'
|
||||
);
|
||||
});
|
||||
|
||||
it('should be invalid with case-insensitive duplicate placeholder names', () => {
|
||||
const addonsLinter = new Linter({ _: ['bar'] });
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(`{
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(
|
||||
`{
|
||||
"duplicate": {
|
||||
"message": "$foo$",
|
||||
"placeholders": {
|
||||
|
@ -214,23 +253,30 @@ describe('LocaleMessagesJSONParser', () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
}`, addonsLinter.collector);
|
||||
}`,
|
||||
addonsLinter.collector
|
||||
);
|
||||
localeMessagesJSONParser.parse();
|
||||
expect(localeMessagesJSONParser.isValid).toEqual(false);
|
||||
const { errors } = addonsLinter.collector;
|
||||
expect(errors.length).toEqual(1);
|
||||
expect(errors[0].code).toEqual(messages.JSON_DUPLICATE_KEY.code);
|
||||
expect(errors[0].description).toContain('Case-insensitive duplicate placeholder name');
|
||||
expect(errors[0].description).toContain(
|
||||
'Case-insensitive duplicate placeholder name'
|
||||
);
|
||||
});
|
||||
|
||||
describe('getLowercasePlaceholders', () => {
|
||||
it('should return undefined if there are no placeholders defined', () => {
|
||||
const addonsLinter = new Linter({ _: ['bar'] });
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(`{
|
||||
const localeMessagesJSONParser = new LocaleMessagesJSONParser(
|
||||
`{
|
||||
"foo": {
|
||||
"message": "foo"
|
||||
}
|
||||
}`, addonsLinter.collector);
|
||||
}`,
|
||||
addonsLinter.collector
|
||||
);
|
||||
localeMessagesJSONParser.parse();
|
||||
const result = localeMessagesJSONParser.getLowercasePlaceholders('foo');
|
||||
expect(result).toEqual(undefined);
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -4,7 +4,8 @@ import PropertiesParser from 'parsers/properties';
|
|||
describe('PropertiesParser', () => {
|
||||
it('should parse valid .properties file correctly', () => {
|
||||
const addonLinter = new Linter({ _: ['bar'] });
|
||||
const propertiesParser = new PropertiesParser(`
|
||||
const propertiesParser = new PropertiesParser(
|
||||
`
|
||||
foo=bar
|
||||
abc.def = xyz
|
||||
#Comment
|
||||
|
@ -14,7 +15,9 @@ describe('PropertiesParser', () => {
|
|||
|
||||
this.is.a =test
|
||||
overwrite=foo
|
||||
overwrite=bar`, addonLinter.collector);
|
||||
overwrite=bar`,
|
||||
addonLinter.collector
|
||||
);
|
||||
|
||||
propertiesParser.parse();
|
||||
|
||||
|
@ -30,7 +33,8 @@ describe('PropertiesParser', () => {
|
|||
|
||||
it('should ignore invalid entities', () => {
|
||||
const addonLinter = new Linter({ _: ['bar'] });
|
||||
const propertiesParser = new PropertiesParser(`
|
||||
const propertiesParser = new PropertiesParser(
|
||||
`
|
||||
this should be ignored.
|
||||
foo=
|
||||
bar
|
||||
|
@ -39,7 +43,9 @@ describe('PropertiesParser', () => {
|
|||
three.lines=a
|
||||
b
|
||||
c
|
||||
d`, addonLinter.collector);
|
||||
d`,
|
||||
addonLinter.collector
|
||||
);
|
||||
|
||||
propertiesParser.parse();
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import * as messages from 'messages';
|
|||
import { VALIDATION_WARNING } from 'const';
|
||||
import CSSScanner from 'scanners/css';
|
||||
|
||||
|
||||
describe('CSS Rule General', () => {
|
||||
it("Check bogus comments don't break parser", async () => {
|
||||
// This code was reported upstream as causing the previous
|
||||
|
|
|
@ -4,7 +4,6 @@ import * as messages from 'messages';
|
|||
import { VALIDATION_WARNING } from 'const';
|
||||
import CSSScanner from 'scanners/css';
|
||||
|
||||
|
||||
describe('CSS Rule InvalidNesting', () => {
|
||||
it('should detect invalid nesting', async () => {
|
||||
const code = oneLine`/* I'm a comment */
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import { VALIDATION_ERROR } from 'const';
|
||||
import { CONTENT_SCRIPT_NOT_FOUND, CONTENT_SCRIPT_EMPTY } from 'messages/javascript';
|
||||
import {
|
||||
CONTENT_SCRIPT_NOT_FOUND,
|
||||
CONTENT_SCRIPT_EMPTY,
|
||||
} from 'messages/javascript';
|
||||
import JavaScriptScanner from 'scanners/javascript';
|
||||
|
||||
function createJsScanner(code, validatedFilename, existingFiles = {}) {
|
||||
|
@ -53,7 +56,11 @@ describe('content_scripts_file_absent', () => {
|
|||
'anotherFolder/contentScript.js': '',
|
||||
'contentScript.js': '',
|
||||
};
|
||||
const jsScanner = createJsScanner(code, fileRequiresContentScript, existingFiles);
|
||||
const jsScanner = createJsScanner(
|
||||
code,
|
||||
fileRequiresContentScript,
|
||||
existingFiles
|
||||
);
|
||||
|
||||
const { linterMessages } = await jsScanner.scan();
|
||||
expect(linterMessages).toEqual([]);
|
||||
|
@ -68,14 +75,19 @@ describe('content_scripts_file_absent', () => {
|
|||
browser.tabs.executeScript({ file: 'contentScript.js' });
|
||||
browser.tabs.executeScript({ file: 'files/script/preload.js' });
|
||||
`;
|
||||
const fileRequiresContentScript = 'files/script/file-requires-content-script.js';
|
||||
const fileRequiresContentScript =
|
||||
'files/script/file-requires-content-script.js';
|
||||
const existingFiles = {
|
||||
'content_scripts/existingFile.js': '',
|
||||
'anotherFolder/contentScript.js': '',
|
||||
'files/script/preload.js': '',
|
||||
'contentScript.js': '',
|
||||
};
|
||||
const jsScanner = createJsScanner(code, fileRequiresContentScript, existingFiles);
|
||||
const jsScanner = createJsScanner(
|
||||
code,
|
||||
fileRequiresContentScript,
|
||||
existingFiles
|
||||
);
|
||||
|
||||
const { linterMessages } = await jsScanner.scan();
|
||||
expect(linterMessages).toEqual([]);
|
||||
|
@ -106,11 +118,16 @@ describe('content_scripts_file_absent', () => {
|
|||
browser.tabs.executeScript;
|
||||
`;
|
||||
const fileRequiresContentScript = 'file-requires-content-script.js';
|
||||
const jsScanner = createJsScanner(code, fileRequiresContentScript, { 'content_scripts/existingFile.js': '' });
|
||||
const jsScanner = createJsScanner(code, fileRequiresContentScript, {
|
||||
'content_scripts/existingFile.js': '',
|
||||
});
|
||||
|
||||
let { linterMessages } = await jsScanner.scan();
|
||||
linterMessages = linterMessages.filter((message) => {
|
||||
return message.code in [CONTENT_SCRIPT_EMPTY.code, CONTENT_SCRIPT_NOT_FOUND.code];
|
||||
return (
|
||||
message.code in
|
||||
[CONTENT_SCRIPT_EMPTY.code, CONTENT_SCRIPT_NOT_FOUND.code]
|
||||
);
|
||||
});
|
||||
expect(linterMessages).toEqual([]);
|
||||
});
|
||||
|
|
|
@ -5,7 +5,6 @@ import { ESLINT_RULE_MAPPING } from 'const';
|
|||
import * as jsRules from 'rules/javascript';
|
||||
import { ignorePrivateFunctions } from 'utils';
|
||||
|
||||
|
||||
describe('Eslint rules object', () => {
|
||||
it('should have keys that match the file names', () => {
|
||||
Object.keys(ignorePrivateFunctions(jsRules)).forEach((jsRule) => {
|
||||
|
@ -15,11 +14,16 @@ describe('Eslint rules object', () => {
|
|||
});
|
||||
|
||||
it('should have files that match the keys', () => {
|
||||
const files = readdirSync('src/rules/javascript')
|
||||
.filter((fileName) => fileName !== 'index.js');
|
||||
const files = readdirSync('src/rules/javascript').filter(
|
||||
(fileName) => fileName !== 'index.js'
|
||||
);
|
||||
files.forEach((fileName) => {
|
||||
expect(Object.prototype.hasOwnProperty.call(
|
||||
ESLINT_RULE_MAPPING, path.parse(fileName).name)).toBe(true);
|
||||
expect(
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
ESLINT_RULE_MAPPING,
|
||||
path.parse(fileName).name
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,14 +11,13 @@ describe('event_listener_fourth', () => {
|
|||
|
||||
const { linterMessages } = await jsScanner.scan();
|
||||
expect(linterMessages.length).toEqual(1);
|
||||
expect(linterMessages[0].code).toEqual(
|
||||
messages.EVENT_LISTENER_FOURTH.code
|
||||
);
|
||||
expect(linterMessages[0].code).toEqual(messages.EVENT_LISTENER_FOURTH.code);
|
||||
expect(linterMessages[0].type).toEqual(VALIDATION_WARNING);
|
||||
});
|
||||
|
||||
it('should allow a false literal', async () => {
|
||||
const code = 'window.addEventListener("click", function(){}, false, false);';
|
||||
const code =
|
||||
'window.addEventListener("click", function(){}, false, false);';
|
||||
const jsScanner = new JavaScriptScanner(code, 'badcode.js');
|
||||
|
||||
const { linterMessages } = await jsScanner.scan();
|
||||
|
@ -26,14 +25,13 @@ describe('event_listener_fourth', () => {
|
|||
});
|
||||
|
||||
it('should not allow a truthy literal', async () => {
|
||||
const code = 'window.addEventListener("click", function(){}, false, "true");';
|
||||
const code =
|
||||
'window.addEventListener("click", function(){}, false, "true");';
|
||||
const jsScanner = new JavaScriptScanner(code, 'badcode.js');
|
||||
|
||||
const { linterMessages } = await jsScanner.scan();
|
||||
expect(linterMessages.length).toEqual(1);
|
||||
expect(linterMessages[0].code).toEqual(
|
||||
messages.EVENT_LISTENER_FOURTH.code
|
||||
);
|
||||
expect(linterMessages[0].code).toEqual(messages.EVENT_LISTENER_FOURTH.code);
|
||||
expect(linterMessages[0].type).toEqual(VALIDATION_WARNING);
|
||||
});
|
||||
|
||||
|
@ -52,9 +50,7 @@ describe('event_listener_fourth', () => {
|
|||
|
||||
const { linterMessages } = await jsScanner.scan();
|
||||
expect(linterMessages.length).toEqual(1);
|
||||
expect(linterMessages[0].code).toEqual(
|
||||
messages.EVENT_LISTENER_FOURTH.code
|
||||
);
|
||||
expect(linterMessages[0].code).toEqual(messages.EVENT_LISTENER_FOURTH.code);
|
||||
expect(linterMessages[0].type).toEqual(VALIDATION_WARNING);
|
||||
});
|
||||
|
||||
|
@ -92,9 +88,7 @@ describe('event_listener_fourth', () => {
|
|||
|
||||
const { linterMessages } = await jsScanner.scan();
|
||||
expect(linterMessages.length).toEqual(1);
|
||||
expect(linterMessages[0].code).toEqual(
|
||||
messages.EVENT_LISTENER_FOURTH.code
|
||||
);
|
||||
expect(linterMessages[0].code).toEqual(messages.EVENT_LISTENER_FOURTH.code);
|
||||
expect(linterMessages[0].type).toEqual(VALIDATION_WARNING);
|
||||
});
|
||||
|
||||
|
@ -105,9 +99,7 @@ describe('event_listener_fourth', () => {
|
|||
|
||||
const { linterMessages } = await jsScanner.scan();
|
||||
expect(linterMessages.length).toEqual(1);
|
||||
expect(linterMessages[0].code).toEqual(
|
||||
messages.EVENT_LISTENER_FOURTH.code
|
||||
);
|
||||
expect(linterMessages[0].code).toEqual(messages.EVENT_LISTENER_FOURTH.code);
|
||||
expect(linterMessages[0].type).toEqual(VALIDATION_WARNING);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -32,7 +32,6 @@ describe('no_eval', () => {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
const invalidCodes = [
|
||||
{
|
||||
code: '(0, eval)("foo")',
|
||||
|
@ -122,9 +121,7 @@ describe('no_eval', () => {
|
|||
});
|
||||
|
||||
code.description.forEach((expectedDescription, idx) => {
|
||||
expect(linterMessages[idx].description).toEqual(
|
||||
expectedDescription
|
||||
);
|
||||
expect(linterMessages[idx].description).toEqual(expectedDescription);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,6 @@ import { VALIDATION_WARNING } from 'const';
|
|||
import JavaScriptScanner from 'scanners/javascript';
|
||||
import { NO_IMPLIED_EVAL } from 'messages';
|
||||
|
||||
|
||||
// These rules were mostly copied and adapted from
|
||||
// https://github.com/eslint/eslint/blob/master/tests/lib/rules/no-implied-eval.js
|
||||
// Please make sure to keep them up-to-date and report upstream errors.
|
||||
|
@ -153,9 +152,7 @@ describe('no_implied_eval', () => {
|
|||
});
|
||||
|
||||
code.description.forEach((expectedDescription, idx) => {
|
||||
expect(linterMessages[idx].description).toEqual(
|
||||
expectedDescription
|
||||
);
|
||||
expect(linterMessages[idx].description).toEqual(expectedDescription);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -49,9 +49,7 @@ describe('no_new_func', () => {
|
|||
});
|
||||
|
||||
code.description.forEach((expectedDescription, idx) => {
|
||||
expect(linterMessages[idx].description).toEqual(
|
||||
expectedDescription
|
||||
);
|
||||
expect(linterMessages[idx].description).toEqual(expectedDescription);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,25 +6,23 @@ import {
|
|||
NO_DOCUMENT_WRITE,
|
||||
} from 'messages';
|
||||
|
||||
|
||||
// These rules were mostly copied and adapted from
|
||||
// https://github.com/mozfreddyb/eslint-plugin-no-unsafe-innerhtml/
|
||||
// Please make sure to keep them up-to-date and report upstream errors.
|
||||
// Some notes are not included since we have our own rules
|
||||
// marking them as invalid (e.g document.write)
|
||||
|
||||
|
||||
describe('no_unsafe_innerhtml', () => {
|
||||
const validCodes = [
|
||||
// innerHTML equals
|
||||
'a.innerHTML = \'\';',
|
||||
"a.innerHTML = '';",
|
||||
'c.innerHTML = ``;',
|
||||
'g.innerHTML = Sanitizer.escapeHTML``;',
|
||||
'h.innerHTML = Sanitizer.escapeHTML`foo`;',
|
||||
'i.innerHTML = Sanitizer.escapeHTML`foo${bar}baz`;',
|
||||
|
||||
// tests for innerHTML update (+= operator)
|
||||
'a.innerHTML += \'\';',
|
||||
"a.innerHTML += '';",
|
||||
'b.innerHTML += "";',
|
||||
'c.innerHTML += ``;',
|
||||
'g.innerHTML += Sanitizer.escapeHTML``;',
|
||||
|
@ -57,7 +55,6 @@ describe('no_unsafe_innerhtml', () => {
|
|||
'w.innerHTML = `<span>${"lulz"+"meh"}</span>`;',
|
||||
];
|
||||
|
||||
|
||||
validCodes.forEach((code) => {
|
||||
it(`should allow the use of innerHTML: ${code}`, async () => {
|
||||
const jsScanner = new JavaScriptScanner(code, 'badcode.js');
|
||||
|
@ -67,7 +64,6 @@ describe('no_unsafe_innerhtml', () => {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
const invalidCodes = [
|
||||
// innerHTML examples
|
||||
{
|
||||
|
@ -142,12 +138,11 @@ describe('no_unsafe_innerhtml', () => {
|
|||
'Use of document.write strongly discouraged.',
|
||||
'Unsafe call to document.write',
|
||||
],
|
||||
id: [
|
||||
NO_DOCUMENT_WRITE.code,
|
||||
UNSAFE_DYNAMIC_VARIABLE_ASSIGNMENT.code],
|
||||
id: [NO_DOCUMENT_WRITE.code, UNSAFE_DYNAMIC_VARIABLE_ASSIGNMENT.code],
|
||||
description: [
|
||||
NO_DOCUMENT_WRITE.description,
|
||||
UNSAFE_DYNAMIC_VARIABLE_ASSIGNMENT.description],
|
||||
UNSAFE_DYNAMIC_VARIABLE_ASSIGNMENT.description,
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'document.write(undefined);',
|
||||
|
@ -155,12 +150,11 @@ describe('no_unsafe_innerhtml', () => {
|
|||
'Use of document.write strongly discouraged.',
|
||||
'Unsafe call to document.write',
|
||||
],
|
||||
id: [
|
||||
NO_DOCUMENT_WRITE.code,
|
||||
UNSAFE_DYNAMIC_VARIABLE_ASSIGNMENT.code],
|
||||
id: [NO_DOCUMENT_WRITE.code, UNSAFE_DYNAMIC_VARIABLE_ASSIGNMENT.code],
|
||||
description: [
|
||||
NO_DOCUMENT_WRITE.description,
|
||||
UNSAFE_DYNAMIC_VARIABLE_ASSIGNMENT.description],
|
||||
UNSAFE_DYNAMIC_VARIABLE_ASSIGNMENT.description,
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'document.writeln(evil);',
|
||||
|
@ -211,7 +205,9 @@ describe('no_unsafe_innerhtml', () => {
|
|||
];
|
||||
|
||||
invalidCodes.forEach((code) => {
|
||||
it(`should not allow the use of innerHTML examples ${code.code}`, async () => {
|
||||
it(`should not allow the use of innerHTML examples ${
|
||||
code.code
|
||||
}`, async () => {
|
||||
const jsScanner = new JavaScriptScanner(code.code, 'badcode.js');
|
||||
|
||||
const { linterMessages } = await jsScanner.scan();
|
||||
|
@ -229,9 +225,7 @@ describe('no_unsafe_innerhtml', () => {
|
|||
});
|
||||
|
||||
code.description.forEach((expectedDescription, idx) => {
|
||||
expect(linterMessages[idx].description).toEqual(
|
||||
expectedDescription
|
||||
);
|
||||
expect(linterMessages[idx].description).toEqual(expectedDescription);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,6 @@ import { VALIDATION_WARNING } from 'const';
|
|||
import JavaScriptScanner from 'scanners/javascript';
|
||||
import * as messages from 'messages';
|
||||
|
||||
|
||||
describe('opendialog_nonlit_uri', () => {
|
||||
it('should provide notice on non-literal uris passed to openDialog', async () => {
|
||||
const code = `var bar="https://whavever";
|
||||
|
@ -11,9 +10,7 @@ describe('opendialog_nonlit_uri', () => {
|
|||
|
||||
const { linterMessages } = await jsScanner.scan();
|
||||
expect(linterMessages.length).toEqual(1);
|
||||
expect(linterMessages[0].code).toEqual(
|
||||
messages.OPENDIALOG_NONLIT_URI.code
|
||||
);
|
||||
expect(linterMessages[0].code).toEqual(messages.OPENDIALOG_NONLIT_URI.code);
|
||||
expect(linterMessages[0].type).toEqual(VALIDATION_WARNING);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,6 @@ import { VALIDATION_WARNING } from 'const';
|
|||
import JavaScriptScanner from 'scanners/javascript';
|
||||
import * as messages from 'messages';
|
||||
|
||||
|
||||
describe('opendialog_remote_uri', () => {
|
||||
it('should warn on remote uris passed to openDialog', async () => {
|
||||
const code = `foo.openDialog("https://foo.com/bar/");
|
||||
|
|
|
@ -10,9 +10,7 @@ describe('unsupported browser APIs', () => {
|
|||
|
||||
const { linterMessages } = await jsScanner.scan();
|
||||
expect(linterMessages.length).toEqual(1);
|
||||
expect(linterMessages[0].message).toEqual(
|
||||
'gcm.register is not supported'
|
||||
);
|
||||
expect(linterMessages[0].message).toEqual('gcm.register is not supported');
|
||||
expect(linterMessages[0].type).toEqual(VALIDATION_WARNING);
|
||||
});
|
||||
|
||||
|
@ -24,9 +22,7 @@ describe('unsupported browser APIs', () => {
|
|||
|
||||
const { linterMessages } = await jsScanner.scan();
|
||||
expect(linterMessages.length).toEqual(1);
|
||||
expect(linterMessages[0].message).toEqual(
|
||||
'gcm.register is not supported'
|
||||
);
|
||||
expect(linterMessages[0].message).toEqual('gcm.register is not supported');
|
||||
expect(linterMessages[0].type).toEqual(VALIDATION_WARNING);
|
||||
});
|
||||
|
||||
|
|
|
@ -14,9 +14,7 @@ describe('widget_module', () => {
|
|||
|
||||
const { linterMessages } = await jsScanner.scan();
|
||||
expect(linterMessages.length).toEqual(1);
|
||||
expect(linterMessages[0].code).toEqual(
|
||||
messages.UNEXPECTED_GLOGAL_ARG.code
|
||||
);
|
||||
expect(linterMessages[0].code).toEqual(messages.UNEXPECTED_GLOGAL_ARG.code);
|
||||
expect(linterMessages[0].type).toEqual(VALIDATION_WARNING);
|
||||
});
|
||||
});
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче