diff --git a/.circleci/config.yml b/.circleci/config.yml index bb780824b..311a9ad1a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,5 +1,8 @@ version: 2.1 +orbs: + stoplight: stoplight/cli@0.0.2 + commands: cached-dependencies: steps: @@ -64,12 +67,6 @@ jobs: steps: - install-and-test - test-node-8: - docker: - - image: circleci/node:8-browsers - steps: - - install-and-test - release: docker: - image: circleci/node:10 @@ -88,11 +85,12 @@ jobs: else npm publish fi - - run: yarn build.binary + - run: npx pkg . --targets linux,macos --out-path ./binaries - persist_to_workspace: root: ./ paths: - binaries + upload_artifacts: docker: - image: circleci/golang:1-stretch @@ -109,10 +107,9 @@ jobs: workflows: commit: jobs: - - test-node-latest + # - test-node-latest TODO bring it back when circle supports node-13 - test-node-12 - test-node-10 - - test-node-8 release: jobs: - release: @@ -129,3 +126,11 @@ workflows: only: /^v.*/ requires: - release + - stoplight/analyze: + filters: + branches: + ignore: /.*/ + tags: + only: /^v.*/ + requires: + - release diff --git a/.dependabot/config.yml b/.dependabot/config.yml new file mode 100644 index 000000000..acf94b6ca --- /dev/null +++ b/.dependabot/config.yml @@ -0,0 +1,35 @@ +version: 1 +update_configs: + - package_manager: "javascript" + directory: "/" + update_schedule: "live" + version_requirement_updates: increase_versions + + commit_message: + prefix: "fix" + include_scope: true + + allowed_updates: + - match: + dependency_name: "@stoplight/*" # match all Stoplight deps, regardless of type + update_type: "all" + - match: + dependency_type: "production" + update_type: "all" + - match: + dependency_type: "development" + update_type: "security" + + - package_manager: "javascript" + directory: "/" + update_schedule: "weekly" # we do not need to check for devDependencies updates on a daily basis + version_requirement_updates: increase_versions + + commit_message: + prefix: "chore" + include_scope: true + + allowed_updates: + - match: + dependency_type: "development" + update_type: "all" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..111ed30db --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 + +[*.{ts,js,yaml,scenario,md}] +indent_style = space +indent_size = 2 + +[*.{ts,js}] +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index 9bcb07184..fe05ed3c5 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -19,6 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: automerge + if: github.actor != 'dependabot-preview[bot]' && github.actor != 'dependabot[bot]' uses: "pascalgn/automerge-action@v0.6.0" env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/markdown-links.yml b/.github/workflows/markdown-links.yml new file mode 100644 index 000000000..058350fef --- /dev/null +++ b/.github/workflows/markdown-links.yml @@ -0,0 +1,16 @@ +name: Check Markdown Links + +on: + pull_request: + branches: + - master + - develop + +jobs: + markdown-link-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + with: + fetch-depth: 1 + - uses: gaurav-nelson/github-action-markdown-link-check@master diff --git a/.gitignore b/.gitignore index c51dcfc6c..ea39399a1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ /binaries /rulesets -/src/__tests__/__fixtures__/oas-functions.json +/__karma__/__fixtures__/ /test-harness/tmp/ # testing diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..348076b95 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +10.15.3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b14b356e..d5e552e88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,44 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [5.0.0] - 2019-12-24 + +### Features +- Alphabetical rule function now supports numeric keys [#730](https://github.com/stoplightio/spectral/issues/730) +- Non-JSON-ish YAML mapping keys are reported [#726](https://github.com/stoplightio/spectral/issues/726) +- CLI: new formatter - text [#822](https://github.com/stoplightio/spectral/issues/822) +- CLI: new formatter - teamcity [#823](https://github.com/stoplightio/spectral/issues/823) +- CLI: new formatter - HTML [#389](https://github.com/stoplightio/spectral/issues/389) +- CLI: custom resolver can be provided leveraging --resolver flag [#717](https://github.com/stoplightio/spectral/issues/717) +- CLI: input can be provided via STDIN [#757](https://github.com/stoplightio/spectral/issues/757) +- Implement ignoreUnsupportedFormats to make it easier to detect unrecognized formats [#678](https://github.com/stoplightio/spectral/issues/678) +- Rule's Given can be an array now [#799](https://github.com/stoplightio/spectral/pull/799) +- Casing built-in function is added [#564](https://github.com/stoplightio/spectral/issues/564) +- New oas rule - `operation-tag-defined` [#704](https://github.com/stoplightio/spectral/pull/704) + +### Changed +- BREAKING: The oas2 and oas3 rulesets have been merged into a single oas ruleset [#773](https://github.com/stoplightio/spectral/pull/773) +- BREAKING: Deprecated Spectral#addRules and Spectral#addFunctions have been removed [#561](https://github.com/stoplightio/spectral/issues/561) +- BREAKING: Some oas rules, such as `example-value-or-externalValue` and `openapi-tags`, are now included in the recommended rulset [#725](https://github.com/stoplightio/spectral/issues/725) [#706](https://github.com/stoplightio/spectral/pull/706) +- BREAKING: The `model-description` and `operation-summary-formatted` rules have been removed [#725](https://github.com/stoplightio/spectral/issues/725) +- BREAKING: The `when` rule property has been removed [#585](https://github.com/stoplightio/spectral/issues/585) +- BREAKING: Rules are set to recommended by default [#719](https://github.com/stoplightio/spectral/pull/719) +- Improved error source detection [#685](https://github.com/stoplightio/spectral/pull/685) +- Error paths point at unresolved document [#839](https://github.com/stoplightio/spectral/pull/839) +- Validation messages contain more consistent error paths [#867](https://github.com/stoplightio/spectral/pull/867) +- CLI: Default `--fail-severity` is now `error`, so getting a `warn`, `info` or a `hint` will not return a exit status code [#706](https://github.com/stoplightio/spectral/pull/706) +- Rulesets no longer require a `rules` property [#652](https://github.com/stoplightio/spectral/pull/652) + +### Fixed +- Circular remote references with JSON pointers are resolved correctly [json-ref-resolver#141](https://github.com/stoplightio/json-ref-resolver/pull/141) +- Local root JSON pointers are resolved correctly [json-ref-resolver#146](https://github.com/stoplightio/json-ref-resolver/pull/146) +- Invalid JSON pointers are reported as errors now [json-ref-resolver#140](https://github.com/stoplightio/json-ref-resolver/pull/140) and [json-ref-resolver#147](https://github.com/stoplightio/json-ref-resolver/pull/147) +- Unixify glob patterns under Windows [#679](https://github.com/stoplightio/spectral/issues/679) +- Improved duplicate keys detection [#751](https://github.com/stoplightio/spectral/issues/751) +- Spectral should be usable in browsers with no crypto module available [#846](https://github.com/stoplightio/spectral/pull/846) +- Falsy values are printed in validation messages [#824](https://github.com/stoplightio/spectral/pull/824) +- Validation results are no longer duplicate [#680](https://github.com/stoplightio/spectral/issues/680), [#737](https://github.com/stoplightio/spectral/pull/737) and [#856](https://github.com/stoplightio/spectral/pull/856) + ## [4.2.0] - 2019-10-08 ### Features diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 36ba260e2..64587f867 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,9 @@ Yarn is a package manager for your code, similar to npm. While you can use npm t 5. Build Spectral: `yarn build` 6. Run Spectral from your local installation: `node dist/cli/index.js lint [openapi_spec_file]` 7. Create a new branch for your work: `git checkout -b [name_of_your_new_branch]` -8. Make changes, add tests, and then run the tests: `yarn test.prod` +8. Make changes, add tests, and then run the tests: `yarn test.prod` and `yarn test.harness` +9. Update the documentation if appropriate. For example, if you added a new rule to an OpenAPI ruleset, +add a description of the rule in `docs/reference/openapi-rules.md`. Now, you are ready to commit & push your changes, and make a pull request to the Spectral repo! 😃 @@ -33,7 +35,7 @@ If this is your first Pull Request on GitHub, here's some [help](https://egghead ## To run tests -We run tests in the two envirnoments that Spectral supports - the browser, and node.js. Browser tests are run in a headless Chrome browser via the Karma test runner, while node.js tests are run via the Jest test runner. +We run tests in the two environments that Spectral supports - the browser, and node.js. Browser tests are run in a headless Chrome browser via the Karma test runner, while node.js tests are run via the Jest test runner. Tests should usually be written for both environments, but there are valid cases when you need to write separate tests for each test runner. To do so, just create a file with `*.karma.test.ts` suffix or `*.jest.test.ts`. A good example of Jest only tests are the tests covering Spectral's CLI functionality - something that obviously is not relevant to the browser context. @@ -61,6 +63,12 @@ Running a specific test: yarn test src/cli/commands/__tests__/lint.test.ts ``` +Running the harness tests (these must pass or the PR merge will be blocked): + +```bash +yarn test.harness +``` + ## Creating an issue We want to keep issues in this repo focused on bug reports and feature requests. diff --git a/README.md b/README.md index 89b80d3f3..909756de4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ ![Spectral logo](img/spectral-banner.png) -[![Test Coverage](https://api.codeclimate.com/v1/badges/1aa53502913a428f40ac/test_coverage)](https://codeclimate.com/github/stoplightio/spectral/test_coverage) -[![Maintainability](https://api.codeclimate.com/v1/badges/1aa53502913a428f40ac/maintainability)](https://codeclimate.com/github/stoplightio/spectral/maintainability) -[![Build Status](https://dev.azure.com/vncz/vncz/_apis/build/status/stoplightio.spectral?branchName=develop)](https://dev.azure.com/vncz/vncz/_build/latest?definitionId=4&branchName=develop) +[![Buy us a tree](https://img.shields.io/badge/Buy%20us%20a%20tree-%F0%9F%8C%B3-lightgreen)](https://offset.earth/stoplightinc) [![CircleCI](https://circleci.com/gh/stoplightio/spectral.svg?style=svg)](https://circleci.com/gh/stoplightio/spectral) A flexible JSON linter with out of the box support for OpenAPI v2 and v3. @@ -79,6 +77,7 @@ If you are using Spectral in your project and want to be listed in the examples - [Mike Ralphson](https://github.com/MikeRalphson) for kicking off the Spectral CLI and his work on Speccy - [Jamund Ferguson](https://github.com/xjamundx) for JUnit formatter - [Sindre Sorhus](https://github.com/sindresorhus) for Stylish formatter +- Julian Laval for HTML formatter ## Support diff --git a/docs/getting-started/rulesets.md b/docs/getting-started/rulesets.md index 9052e852f..140cd71e2 100644 --- a/docs/getting-started/rulesets.md +++ b/docs/getting-started/rulesets.md @@ -18,7 +18,9 @@ rules: function: truthy ``` +Spectral has a built-in set of functions such as `truthy` or `pattern`, which you can reference in your rules. The example above adds a single rule that checks that tags objects have a description property defined. +Since v5.0, each rule is recommended by default. Prior to that the default was to be not recommended. It is best to be explicit and set `recommended: true` or `recommended: false` if a ruleset is likely to be used across multiple versions. Running `spectral lint` on the following object with the ruleset above will result in an error being reported, since the tag does not have a description: @@ -76,9 +78,8 @@ Specifying the format is optional, so you can completely ignore this if all the ```yaml rules: - api-servers: + oas3-api-servers: description: "OpenAPI `servers` must be present and non-empty array." - recommended: true formats: ["oas3"] given: "$" then: @@ -99,7 +100,7 @@ Alternatively, formats can be specified at the ruleset level: ```yaml formats: ["oas3"] rules: - api-servers: + oas3-api-servers: description: "OpenAPI `servers` must be present and non-empty array." recommended: true given: "$" @@ -118,9 +119,15 @@ Now all the rules in this ruleset will only be applied if the specified format i Custom formats can be registered via the [JS API](../guides/javascript.md), but the CLI is limited to using the predefined ones. +## Given + +The `given` keyword is, besides `then`, the only required property on each rule definition. +It can be any valid JSONPath expression or an array of JSONPath expressions. +[JSONPath Online Evaluator](http://jsonpath.com/) is a helpful tool to determine what `given` path you want. + ## Severity -The `severity` keyword is optional and can be `error`, `warn`, `info`, or `hint`. +The `severity` keyword is optional and can be `error`, `warn`, `info`, or `hint`. The default value is `warn`. ## Then @@ -155,12 +162,10 @@ responses: ## Extending rules -Rulesets can extend other rulesets. For example, Spectral comes with two built in rulesets - one for OpenAPI v2 (`spectral:oas2`), and one for OpenAPI v3 (`spectral:oas3`). - -Use the `extends` property in your ruleset file to build upon or customize other rulesets. +Rulesets can extend other rulesets using the `extends` property. This can be used to build upon or customize other rulesets. ```yaml -extends: spectral:oas2 +extends: spectral:oas rules: my-rule-name: description: Tags must have a description. @@ -185,45 +190,62 @@ extends: Sometimes you might want to apply specific rules from another ruleset. Use the `extends` property, and pass `off` as the second argument in order to add the rules from another ruleset, but disable them all by default. This allows you to pick and choose which rules you would like to enable. ```yaml -extends: [[spectral:oas2, off]] +extends: [[spectral:oas, off]] rules: - # This rule is defined in the spectral:oas2 ruleset. We're passing `true` to turn it on and inherit the severity defined in the spectral:oas2 ruleset. + # This rule is defined in the spectral:oas ruleset. We're passing `true` to turn it on and inherit the severity defined in the spectral:oas ruleset. operation-operationId-unique: true ``` -The example above will run the single rule that we enabled, since we passed `off` to disable all rules by default when extending the `spectral:oas2` ruleset. +The example above will run the single rule that we enabled, since we passed `off` to disable all rules by default when extending the `spectral:oas` ruleset. ## Disabling rules This example shows the opposite of the "Enabling Specific rules" example. Sometimes you might want to enable all rules by default, and disable a few. ```yaml -extends: [[spectral:oas2, all]] +extends: [[spectral:oas, all]] rules: operation-operationId-unique: false ``` -The example above will run all of the rules defined in the `spectral:oas2` ruleset (rather than the default behavior that runs only the recommended ones), with one exceptions - we turned `operation-operationId-unique` off. +The example above will run all of the rules defined in the `spectral:oas` ruleset (rather than the default behavior that runs only the recommended ones), with one exceptions - we turned `operation-operationId-unique` off. -The current recommended rules are marked with the property `recommended: true` in their respective rulesets: +- [Rules relevant to OpenAPI v2 and v3](../reference/openapi-rules.md) -- [Rules relevant to both OpenAPI v2 and v3](https://github.com/stoplightio/spectral/tree/master/src/rulesets/oas/index.json) -- [Rules specific to only OpenAPI v2](https://github.com/stoplightio/spectral/tree/master/src/rulesets/oas2/index.json) -- [Rules specific to only OpenAPI v3](https://github.com/stoplightio/spectral/tree/master/src/rulesets/oas3/index.json) ## Changing rule severity ```yaml -extends: spectral:oas2 +extends: spectral:oas rules: operation-2xx-response: warn ``` -The example above will run the recommended rules from the `spectral:oas2` ruleset, but report `operation-2xx-response` as a warning rather than as an error (as is the default behavior in the `spectral:oas2` ruleset). +The example above will run the recommended rules from the `spectral:oas` ruleset, but report `operation-2xx-response` as a warning rather than as an error (as is the default behavior in the `spectral:oas` ruleset). Available severity levels are `error`, `warn`, `info`, `hint`, and `off`. +## Enriching rule messages + +To help you create meaningful error messages, Spectral comes with a couple of placeholders that are evaluated at runtime. + +- {{error}} - the error returned by function +- {{description}} - the description set on the rule +- {{path}} - the whole error path +- {{property}} - the last segment of error path +- {{value}} - the linted value +### Examples + +```yaml +message: "{{error}}" # will output the message generated by then.function + +message: "The value of '{{property}}' property must equal 'foo'" + +message: "{{value}} is greater than 0" + +message: "{{path}} cannot point at remote reference" +``` ## Creating custom functions Learn more about [custom functions](../guides/custom-functions.md). diff --git a/docs/guides/cli.md b/docs/guides/cli.md index 8e2e4a025..645d86716 100644 --- a/docs/guides/cli.md +++ b/docs/guides/cli.md @@ -26,12 +26,14 @@ Other options include: --encoding, -e text encoding to use [string] [default: "utf8"] --format, -f formatter to use for outputting results [string] [default: "stylish"] --output, -o output to a file instead of stdout [string] + --resolver path to custom json-ref-resolver instance [string] --ruleset, -r path/URL to a ruleset file [string] --skip-rule, -s ignore certain rules if they are causing trouble [string] --fail-severity, -F results of this level or above will trigger a failure exit code - [string] [choices: "error", "warn", "info", "hint"] [default: "hint"] + [string] [choices: "error", "warn", "info", "hint"] [default: "warn"] --display-only-failures, -D only output results equal to or greater than --fail-severity [boolean] [default: false] + --ignore-unknown-format do not warn about unmatched formats [boolean] [default: false] --verbose, -v increase verbosity [boolean] --quiet, -q no logging - output only [boolean] ``` @@ -44,14 +46,35 @@ Here you can build a [custom ruleset](../getting-started/rulesets.md), or extend ## Error Results -Spectral has a few different error severities: `error`, `warn`, `info` and `hint`, and they are in "order" from highest to lowest. By default, all results will be shown regardless of severity, and the presence of any results will cause a failure status code of 1. +Spectral has a few different error severities: `error`, `warn`, `info` and `hint`, and they are in "order" from highest to lowest. By default, all results will be shown regardless of severity, but since v5.0, only the presence of errors will cause a failure status code of 1. Seeing results and getting a failure code for it are now two different things. -The default behavior is can be modified with the `--fail-severity=` option. Setting fail severity to `--fail-severity=warn` would return a status code of 1 for any warning results or higher, so that would also include error. Using `--fail-severity=error` will only show errors. +The default behavior can be modified with the `--fail-severity=` option. Setting fail severity to `--fail-severity=info` would return a failure status code of 1 for any info results or higher. Using `--fail-severity=warn` will cause a failure status code for errors or warnings. -Changing the fail severity will not effect output. To change what results Spectral CLI prints to the screen, add the `--display-only-failures` switch (or just `-D` for short). This will strip out any results which are below the fail severity. +Changing the fail severity will not effect output. To change what results Spectral CLI prints to the screen, add the `--display-only-failures` switch (or just `-D` for short). This will strip out any results which are below the specified fail severity. ## Proxying To have requests made from Spectral be proxied through a server, you'd need to specify PROXY environment variable: `PROXY=<> spectral lint spec.yaml` + +## Custom $ref resolving + +If you want to customize $ref resolving, you can leverage `--resolver` flag and pass a path to the JS file exporting a custom instance of json-ref-resolver Resolver. + +### Example + +Assuming the filename is called `my-resolver.js` and the content looks as follows, the path should look more or less like `--resolver=./my-resolver.js`. + +```js +const { Resolver } = require('@stoplight/json-ref-resolver'); + +module.exports = new Resolver({ + resolvers: { + // pass any resolver for protocol you need + } +}); +``` + + +You can learn more about $ref resolving in the [JS section](./javascript.md#using-custom-resolver). diff --git a/docs/guides/custom-functions.md b/docs/guides/custom-functions.md index 6c30fcba5..56823a96d 100644 --- a/docs/guides/custom-functions.md +++ b/docs/guides/custom-functions.md @@ -13,6 +13,43 @@ export type IFunction = ( ) => void | IFunctionResult[]; ``` +### targetValue + +`targetValue` the value the custom function is provided with and is supposed to lint against. +It's based on `given` JSONPath expression defined on the rule and optionally `field` if placed on `then`. +For instance, given the following partial of OpenAPI 3.0 document +```yaml +openapi: 3.0.0 +info: + title: foo +``` +and the following `given` JSONPath expression `$`, `targetValue` would be a JS object literal containing `openapi` and `info` properties. +If you changed the path to `$.info.title`, `targetValue` would equal `"foo"`. + +### options + +Options corresponds to `functionOptions` that's defined in `then` property of each rule. +Each rule can specify options that each function should receive. This can be done as follows + +```yaml +operation-id-kebab-case: + given: "$" + then: + function: pattern + functionOptions: # this object be passed down as options to the custom function + match: ^[a-z][a-z0-9\-]*$ +``` + +### paths + +`paths.given` contains JSONPath expression you set in a rule - in `given` field. +If a particular rule has a `field` property in `then`, that path will be exposed as `paths.target`. + +### otherValues + +`otherValues.original` and `otherValues.given` are equal for the most of time and represent the value matched using JSONPath expression. +`otherValues.resolved` serves for internal purposes, therefore we discourage using it in custom functions. + Custom functions take exactly the same arguments as built-in functions do, so you are more than welcome to take a look at the existing implementation. The process of creating a function involves 2 steps: @@ -129,7 +166,7 @@ module.exports = (obj) => { }; ``` -You do not need to provide any shim for `Object.entries` or use [regenerator](https://facebook.github.io/regenerator/) for `for of` loop. As stated, you cannot use ES Modules, so the following code is considered sa invalid and won't work correctly. +You do not need to provide any shim for `Object.entries` or use [regenerator](https://facebook.github.io/regenerator/) for the `for of` loop. As stated, you cannot use ES Modules, so the following code is considered as invalid and won't work correctly. ```js export default (obj) => { @@ -153,4 +190,4 @@ module.exports = (obj) => { If you have any module system, you need to use some bundler, preferably Rollup.js as it generates efficient bundles. -We are still evaluating the idea of supporting ESModule and perhaps we decide to bring support for ES Modules at some point, yet for now you cannot use them. +We are still evaluating the idea of supporting ESModule and perhaps we will decide to bring support for ES Modules at some point, yet for now you cannot use them. diff --git a/docs/guides/javascript.md b/docs/guides/javascript.md index 0dbf49b6e..3cf73fb47 100644 --- a/docs/guides/javascript.md +++ b/docs/guides/javascript.md @@ -8,24 +8,28 @@ Assuming it has been installed as a Node module via NPM/Yarn, it can be used to ```js const { Spectral } = require('@stoplight/spectral'); -const { parseWithPointers } = require("@stoplight/yaml"); +const { getLocationForJsonPath, parseWithPointers } = require("@stoplight/yaml"); -const myOpenApiDocument = parseWithPointers(` -responses: +const myOpenApiDocument = parseWithPointers(`responses: '200': description: '' schema: $ref: '#/definitions/error-response' `); -// create a new instance of spectral with all of the baked in rulesets const spectral = new Spectral(); -spectral.run(myOpenApiDocument).then(results => console.log(results)); +spectral + .run({ + parsed: myOpenApiDocument, + getLocationForJsonPath, + }) + .then(console.log); ``` Please note that by default Spectral supports YAML 1.2 with merge keys extension. -This will run Spectral with no rules or functions, so it's not going to do anything. Find out how to add rules and functions below. +This will run Spectral with no formats, rules or functions, so it's not going to do anything besides $ref resolving. +Find out how to add formats, rules and functions below. ## Linting an Object @@ -33,7 +37,6 @@ Instead of passing a string to `parseWithPointers`, you can pass in JavaScript o ```js const { Spectral } = require('@stoplight/spectral'); -const { parseWithPointers } = require("@stoplight/yaml"); const myOpenApiDocument = { responses: { @@ -47,302 +50,180 @@ const myOpenApiDocument = { }; const spectral = new Spectral(); -spectral.run(myOpenApiDocument).then(results => console.log(results); +spectral.run(myOpenApiDocument).then(console.log); ``` +Note - this usage is discouraged, since you won't get accurate ranges. + +## Registering Formats + +If you are interested in linting OpenAPI documents or JSON Schema models, you may need to register formats. +Assuming your rulesets use the built-in Spectral formats, this can be accomplished as follows + +- OpenAPI + +```js +const { Spectral, isOpenApiv2, isOpenApiv3 } = require('@stoplight/spectral'); + +const spectral = new Spectral(); +spectral.registerFormat('oas2', isOpenApiv2); +spectral.registerFormat('oas3', isOpenApiv3); +``` + +- JSON Schema + +```js +const { + Spectral, + isJSONSchema, + isJSONSchemaDraft4, + isJSONSchemaDraft6, + isJSONSchemaDraft7, + isJSONSchemaDraft2019_09, + isJSONSchemaLoose, +} = require('@stoplight/spectral'); + +const spectral = new Spectral(); +spectral.registerFormat('json-schema', isJSONSchema); +spectral.registerFormat('json-schema-loose', isJSONSchemaLoose); +spectral.registerFormat('json-schema-draft4', isJSONSchemaDraft4); +spectral.registerFormat('json-schema-draft6', isJSONSchemaDraft6); +spectral.registerFormat('json-schema-draft7', isJSONSchemaDraft7); +spectral.registerFormat('json-schema-2019-09', isJSONSchemaDraft2019_09); +``` + +Learn more about predefined formats in the (ruleset documentation)[../getting-started/rulesets.md#formats]. + ## Loading Rules -Spectral comes with some rulesets that are very specific to OpenAPI v2/v3, and they can be loaded using `Spectral.loadRuleset()`. +Spectral comes with some rulesets that are very specific to OpenAPI v2/v3, and they can be loaded using `Spectral.loadRuleset()`. ```js -const { Spectral } = require('@stoplight/spectral'); -const { parseWithPointers } = require("@stoplight/yaml"); +const { Spectral, isOpenApiv2, isOpenApiv3 } = require('@stoplight/spectral'); -const myOpenApiDocument = { - // any parsed open api document -}; +const myOpenApiDocument = ` +openapi: 3.0.0 +# here goes the rest of document +` const spectral = new Spectral(); -spectral.loadRuleset('spectral:oas3') // spectral:oas2 for OAS 2.0 aka Swagger +spectral.registerFormat('oas2', isOpenApiv2); // spectral:oas2 for OpenAPI v2.0 +spectral.registerFormat('oas3', isOpenApiv3); +spectral.loadRuleset('spectral:oas') .then(() => spectral.run(myOpenApiDocument)) .then(results => { console.log('here are the results', results); }); -``` - -[Try it out!](https://repl.it/@ChrisMiaskowski/spectral-rules-example) - -
Click to see the output -

- -```bash -[ - { - "code": "invalid-ref", - "path": [ - "responses", - "200", - "schema", - "$ref" - ], - "message": "'#/definitions/error-response' does not exist", - "severity": 0, - "range": { - "start": { - "line": 5, - "character": 16 - }, - "end": { - "line": 5, - "character": 46 - } - } - }, - { - "code": "info-contact", - "message": "Info object should contain `contact` object.", - "path": [ - "info", - "contact" - ], - "severity": 1, - "range": { - "start": { - "line": 0, - "character": 0 - }, - "end": { - "line": 5, - "character": 46 - } - } - }, - { - "code": "info-description", - "message": "OpenAPI object info `description` must be present and non-empty string.", - "path": [ - "info", - "description" - ], - "severity": 1, - "range": { - "start": { - "line": 0, - "character": 0 - }, - "end": { - "line": 5, - "character": 46 - } - } - }, - { - "code": "oas3-schema", - "message": "should NOT have additional properties: responses", - "path": [], - "severity": 0, - "range": { - "start": { - "line": 0, - "character": 0 - }, - "end": { - "line": 5, - "character": 46 - } - } - }, - { - "code": "api-servers", - "message": "OpenAPI `servers` must be present and non-empty array.", - "path": [ - "servers" - ], - "severity": 1, - "range": { - "start": { - "line": 0, - "character": 0 - }, - "end": { - "line": 5, - "character": 46 - } - } - } -] ``` -

-
- -The OpenAPI rules are opinionated. There might be some rules that you prefer to change. We encourage you to create your rules to fit your use case. We welcome additions to the existing rulesets as well! +The OpenAPI rules are opinionated. There might be some rules that you prefer to change, or disable. We encourage you to create your rules to fit your use case, and we welcome additions to the existing rulesets as well! ## Advanced -### Creating a custom rule +### Creating a custom format -Spectral has a built-in set of functions which you can reference in your rules. This example uses the `RuleFunction.PATTERN` to create a rule that checks that all property values are in snake case. +Spectral supports two core formats: `oas2` and `oas3`. Using `registerFormat` you can add support for auto-detecting other formats. You might want to do this for a ruleset which is run against multiple major versions of description format like RAML v0.8 and v1.0. -```javascript +```js const { Spectral } = require('@stoplight/spectral'); const spectral = new Spectral(); -spectral.addRules({ - snake_case: { - description: 'Checks for snake case pattern', - - // evaluate every property - given: '$..*', +spectral.registerFormat('foo-bar', obj => typeof obj === 'object' && obj !== null && 'foo-bar' in obj); - then: { - function: 'pattern', - functionOptions: { - match: '^[a-z]+[a-z0-9_]*[a-z0-9]+$', +spectral.setRuleset({ + functions: {}, + rules: { + rule1: { + given: '$.x', + formats: ['foo-bar'], + severity: 'error', + then: { + function: 'truthy', }, }, }, }); -// run! -spectral.run({name: 'helloWorld',}).then(results => { - console.log(results); -}); +spectral + .run({ + 'foo-bar': true, + x: false + }) + .then(result => { + expect(result).toEqual([ + expect.objectContaining({ + code: 'rule1', + }), + ]); + }); ``` -[Try it out!](https://repl.it/@ChrisMiaskowski/spectral-pattern-example) - -```bash -[ - { - "code": "snake_case", - "message": "Checks for snake case pattern", - "path": [ - "name" - ], - "severity": 1, - "range": { - "start": { - "line": 1, - "character": 10 - }, - "end": { - "line": 1, - "character": 22 - } - } - } -] -``` +### Using custom resolver -### Creating a custom function +Spectral lets you provide any custom $ref resolver. By default, http(s) and file protocols are resolved, relatively to +the document Spectral lints against. If you'd like support any additional protocol or adjust the resolution, you are +absolutely fine to do it. In order to achieve that, you need to create a custom json-ref-resolver instance. -Sometimes the built-in functions don't cover your use case. This example creates a custom function, `customNotThatFunction`, and then uses it within a rule, `openapi_not_swagger`. The custom function checks that you are not using a specific string (e.g., "Swagger") and suggests what to use instead (e.g., "OpenAPI"). +You can find more information about how to create custom resolvers in +the [@stoplight/json-ref-resolver](https://github.com/stoplightio/json-ref-resolver) repository. -```javascript +```js +const path = require('path'); +const fs = require('fs'); const { Spectral } = require('@stoplight/spectral'); - -// custom function -const customNotThatFunction = (targetValue, options) => { - const { match, suggestion } = options; - - if (targetValue && targetValue.match(new RegExp(match))) { - // return the single error - return [ - { - message: `Use ${suggestion} instead of ${match}!`, - }, - ]; +const { Resolver } = require('@stoplight/json-ref-resolver'); + +const customFileResolver = new Resolver({ + resolvers: { + file: { + resolve: ref => { + return new Promise((resolve, reject) => { + const basePath = process.cwd(); + const refPath = ref.path(); + fs.readFile(path.join(basePath, refPath), 'utf8', (err, data) => { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); + } + } } -}; - -const spectral = new Spectral(); - -spectral.addFunctions({ - notThat: customNotThatFunction, }); -spectral.addRules({ - openapi_not_swagger: { - description: 'Checks for use of Swagger, and suggests OpenAPI.', - - // check every property - given: '$..*', +const spectral = new Spectral({ resolver: customFileResolver }); - then: { - // reference the function we added! - function: 'notThat', +// lint document as usual +``` - // pass it the options it needs - functionOptions: { - match: 'Swagger', - suggestion: 'OpenAPI', - }, - }, - }, -}); +The custom resolver we've just created will resolve all remote file refs relatively to the current working directory. -// run! -spectral.run({description: 'Swagger is pretty cool!',}).then(results => { - console.log(JSON.stringify(results, null, 4)); -}); -``` +More on that can be found in the [json-ref-resolver repo](https://github.com/stoplightio/json-ref-resolver). -[Try it out!](https://repl.it/@ChrisMiaskowski/spectral-custom-function-example) - -```bash -# Outputs a single result since we are using the term `Swagger` in our object -[ - { - "code": "openapi_not_swagger", - "message": "Checks for use of Swagger, and suggests OpenAPI.", - "path": [ - "description" - ], - "severity": 1, - "range": { - "start": { - "line": 1, - "character": 17 - }, - "end": { - "line": 1, - "character": 42 - } - } - } -] -``` +### Using custom de-duplication strategy -For more information on creating rules, read about [rulesets](../getting-started/rulesets.md). +By default, Spectral will de-duplicate results based on the result code and document location. You can customize this +behavior with the `computeFingerprint` option. For example, here is the default fingerprint implementation: -### Creating a custom format +The final reported results are de-duplicated based on their computed fingerprint. -Spectral supports two core formats: `oas2` and `oas3`. Using `registerFormat` you can add support for autodetecting other formats. You might want to do this for a ruleset which is run against multiple major versions of description format like RAML v0.8 and v1.0. +```ts +const spectral = new Spectral({ + computeFingerprint: (rule: IRuleResult, hash) => { + let id = String(rule.code); -```js -spectral.registerFormat('foo-bar', obj => typeof obj === 'object' && obj !== null && 'foo-bar' in obj); + if (rule.path && rule.path.length) { + id += JSON.stringify(rule.path); + } else if (rule.range) { + id += JSON.stringify(rule.range); + } -spectral.addRules({ - rule1: { - given: '$.x', - formats: ['foo-bar'], - severity: 'error', - then: { - function: 'truthy', - }, - } -}); + if (rule.source) id += rule.source; -const result = await spectral.run({ - 'foo-bar': true, - x: false + return hash(id); + }, }); - -expect(result).toEqual([ - expect.objectContaining({ - code: 'rule1', - }), -]); ``` diff --git a/docs/migration-guides/5.0.md b/docs/migration-guides/5.0.md new file mode 100644 index 000000000..7c93b2cd5 --- /dev/null +++ b/docs/migration-guides/5.0.md @@ -0,0 +1,154 @@ +# Spectral v4 to v5 Migration Guide + +Our docs have been updated, so you can always refer to them. To make the transition less painful, +this migration guide covers the most notable changes. + +### I have my own custom rulesets... + +1. oas2 and oas3 rulesets have been merged to oas + +From now on, you don't need to worry about oas2 or oas3, you simply extend oas ruleset and Spectral will figure out which rules to apply based on given formats. + +**Spectral v4**: + +```json +{ + "extends": ["spectral:oas2", "spectral:oas3"] +} +``` + +**Spectral v5**: + +```json +{ + "extends": ["spectral:oas"] +} +``` + +2. All rules are recommended by default now + +Previously it wasn't quite clear that you need to make rule recommended in order to have it enabled by default. +We addressed it and right now you need to explicitly mark rule as unrecommended assuming you do want it to be so. + +**Spectral v4**: + +```json +{ + "rules": { + "true-info-contact": { + "message": "Info must contain Stoplight", + "given": "$.info.contact", + "then": { + "function": "truthy" + } + } + } +} +``` + +**Spectral v5**: + +```json +{ + "rules": { + "true-info-contact": { + "message": "Info must contain Stoplight", + "given": "$.info.contact", + "recommended": false, + "then": { + "function": "truthy" + } + } + } +} +``` + +3. `when` has been dropped + +`when` wasn't particularly useful and we haven't seen any real-world usage of it, even internally in our own rulesets. +Since there is a way to write a rule avoiding `when` we decided to get rid of it to reduce complexity. + +**Spectral v4**: + +```json +{ + "rules": { + "error-response-description": { + "given": "$.responses[*]", + "when": { + "field": "@key", + "pattern": "^4.*" + }, + "then": { + "field": "description", + "function": "truthy" + } + } + } +} +``` + +**Spectral v5**: + +```json +{ + "rules": { + "error-response-description": { + "given": "$.responses.[?(@property >= 400 && @property < 500)]", + "then": { + "field": "description", + "function": "truthy" + } + } + } +} +``` + +### I use Spectral programmatically via API... + +1. addFunctions and addRules have been removed + +We strongly encourage everyone to write rulesets, therefore the new preferred way to load a ruleset is via `loadRuleset`. + +**Spectral v4**: + +```js +const { oas3Functions, rules: oas3Rules } = require('@stoplight/spectral/dist/rulesets/oas3'); + +const spectral = new Spectral(); +spectral.addFunctions(oas3Functions); +spectral.addFunctions(oas3Rules); +spectral.run(myOpenApiDocument) + .then(results => { + console.log('here are the results', results); + }); +``` + +**Spectral v5**: + +```js +const { Spectral } = require('@stoplight/spectral'); + +const spectral = new Spectral(); +spectral.loadRuleset('spectral:oas') + .then(() => spectral.run(myOpenApiDocument)) + .then(results => { + console.log('here are the results', results); + }); +``` + +Alternatively, if your ruleset is stored in a plain JSON file, you can also consider using `setRuleset`, as follows + +```js +const { Spectral } = require('@stoplight/spectral'); +const ruleset = require('./my-ruleset.json'); + +const spectral = new Spectral(); +spectral.setRuleset(ruleset); +spectral.run(myOpenApiDocument) + .then(results => { + console.log('here are the results', results); + }); +``` + + diff --git a/docs/reference/functions.md b/docs/reference/functions.md index dce980022..10b89cc24 100644 --- a/docs/reference/functions.md +++ b/docs/reference/functions.md @@ -18,6 +18,7 @@ keyedBy | key to sort an object by | no openapi-tags-alphabetical: description: OpenAPI object should have alphabetical `tags`. type: style + recommended: true given: "$" then: field: tags @@ -100,9 +101,45 @@ notMatch | if provided, value must _not_ match this regex | no notMatch: ".+\/$" ``` +## casing + +Text must match a certain case, like `camelCase` or `snake_case`. + + + +name | description | required? +---------|----------|--------- +type | the casing type to match against | yes +disallowDigits | if not truthy, digits are allowed | no + +Available types are: + +| name | sample | +|--------|----------------| +| flat | verylongname | +| camel | veryLongName | +| pascal | VeryLongName | +| kebab | very-long-name | +| cobol | VERY-LONG-NAME | +| snake | very_long_name | +| macro | VERY_LONG_NAME | + + + +```yaml +camel-case-name: + description: Name should camelCased. + type: style + given: "$.name" + then: + function: casing + functionOptions: + type: camel +``` + ## schema -Use JSON Schema (draft 7) to treat the contents of the $given JSON Path as a JSON instance. +Use JSON Schema (draft 4, 6 or 7) to treat the contents of the $given JSON Path as a JSON instance. @@ -113,7 +150,7 @@ schema | a valid JSON Schema document | yes ```yaml -api-servers: +oas3-api-servers: description: "OpenAPI `servers` must be present and non-empty array." recommended: true type: "style" @@ -165,7 +202,36 @@ The value should not be `false`, `""`, `0`, `null` or `undefined`. Basically any The value must be `undefined`. When combined with `field: foo` on an object the `foo` property must be undefined. -_**Note:** Due to the way YAML works, just having `foo: ` with no value set is not the same as being `undefined`. This would be `falsy`. +_**Note:** Due to the way YAML works, just having `foo: ` with no value set is not the same as being `undefined`. This would be `falsy`._ + +## unreferencedReusableObject + +This function identifies unreferenced objects within a document. + +For it to properly operate, `given` should point to the member holding the potential reusable objects. + +_Warning:_ This function may identify false positives when used against a specification that acts as a library (a container storing reusable objects, leveraged by other specifications that reference those objects). + + + +name | description | required? +---------|----------|--------- +reusableObjectsLocation | a local json pointer to the document member holding the reusable objects (eg. `#/definitions` for an OAS2 document, `#/components/schemas` for an OAS3 document). | yes + + + +``` yaml +unused-definition: + description: Potentially unused definition has been detected. + recommended: true + type: style + resolved: false + given: "$.definitions" + then: + function: unreferencedReusableObject + functionOptions: + reusableObjectsLocation: "#/definitions" +``` ## xor diff --git a/docs/reference/openapi-rules.md b/docs/reference/openapi-rules.md index 4442a4ccb..7a86f073e 100644 --- a/docs/reference/openapi-rules.md +++ b/docs/reference/openapi-rules.md @@ -1,18 +1,12 @@ # OpenAPI Rules -Spectral has three rulesets built in: +Spectral has a built-in "oas" ruleset, with OAS being shorthand for the [OpenAPI Specification](https://openapis.org/specification). -- [oas](#oas) -- [oas2](#oas2) -- [oas3](#oas3) +In your ruleset file you can add `extends: "spectral:oas"` and you'll get all of the following rules applied, depending on the appropriate OpenAPI version used (detected through [formats](../getting-started/rulesets.md#formats)). -_OAS is shorthand for OpenAPI Specification._ +## OpenAPI v2 & v3 -In your ruleset file you can add `extends: "spectral:oas"` and you'll get everything from `spectral:oas2` and `spectral:oas3`, meaning you can lint either type of document and only the relevant rules will apply thanks to [formats](../getting-started/rulesets.md#formats). - -Let's look at the rules in these rulesets. - -## oas +These rules apply to both OpenAPI v2 and v3. ### operation-2xx-response @@ -120,7 +114,7 @@ info: Examples for `requestBody` or response examples can have an `externalValue` or a `value`, but they cannot have both. -**Recommended:** No +**Recommended:** Yes **Bad Example** @@ -222,7 +216,7 @@ info: This rule protects against an edge case, for anyone bringing in description documents from third parties and using the parsed content rendered in HTML/JS. If one of those third parties does something shady like inject `eval()` JavaScript statements, it could lead to an XSS attack. -**Recommended:** No +**Recommended:** Yes **Bad Example** @@ -283,7 +277,7 @@ Why? Well, you _can_ reference tags arbitrarily in operations, and definition is Defining tags allows you to add more information like a `description`. For more information see [tag-description](#tag-description). -**Recommended:** No +**Recommended:** Yes ### operation-default-response @@ -324,6 +318,11 @@ Use just one tag for an operation, which is helpful for some documentation syste ### operation-summary-formatted + +> ### Removed in v5.0 +> +> This rule was removed in Spectral v5.0, so if you are relying on it you can find the [old definition here](https://github.com/stoplightio/spectral/blob/v4.2.0/src/rulesets/oas/index.json#L312) and paste it into your [custom ruleset](../getting-started/rulesets.md). + Operation `summary` should start with upper case and end with a dot. **Recommended:** No @@ -334,6 +333,12 @@ Operation should have non-empty `tags` array. **Recommended:** Yes +### operation-tag-defined + +Operation tags should be defined in global tags. + +**Recommended:** Yes + ### path-declarations-must-exist Path parameter declarations cannot be empty, ex.`/given/{}` is invalid. @@ -375,53 +380,57 @@ tags: **Recommended:** No -## oas2 +## OpenAPI v2.0-only -These OpenAPI v2.0-only rules can be loaded with `extends: "spectral:oas2"` and include all the oas rules. +These rules will only apply to OpenAPI v2.0 documents. -### operation-formData-consume-check +### oas2-operation-formData-consume-check Operations with an `in: formData` parameter must include `application/x-www-form-urlencoded` or `multipart/form-data` in their `consumes` property. **Recommended:** Yes -### api-host +### oas2-api-host OpenAPI `host` must be present and non-empty string. **Recommended:** Yes -### api-schemes +### oas2-api-schemes OpenAPI host `schemes` must be present and non-empty array. **Recommended:** Yes -### host-not-example +### oas2-host-not-example -Server URL should not point at `example.com`. +Server URL should not point at example.com. **Recommended:** No -### host-trailing-slash +### oas2-host-trailing-slash Server URL should not have a trailing slash. **Recommended:** Yes -### model-description +### oas2-operation-security-defined -Definition `description` must be present and non-empty string. +Operation `security` values must match a scheme defined in the `securityDefinitions` object. -**Recommended:** No +**Recommended:** Yes -### operation-security-defined +### oas2-unused-definition -Operation `security` values must match a scheme defined in the `securityDefinitions` object. +Potential unused reusable `definition` entry has been detected. + +_Warning:_ This rule may identify false positives when linting a specification +that acts as a library (a container storing reusable objects, leveraged by other +specifications that reference those objects). **Recommended:** Yes -### valid-example +### oas2-valid-example Examples must be valid against their defined schema. @@ -451,11 +460,11 @@ Parameter objects should have a `description`. **Recommended:** No -## oas3 +## OpenAPI v3-only -These OpenAPI v3.0-only rules can be loaded with `extends: "spectral: oas3"` and include all the oas rules. +These rules will only be applied to OpenAPI v3.0 documents. -### api-servers +### oas3-api-servers OpenAPI `servers` must be present and non-empty array. @@ -475,22 +484,15 @@ servers: If this is going out to the world, maybe have production and a general sandbox people can play with. - -### model-description - -Model `description` must be present and non-empty string. - -**Recommended:** No - -### operation-security-defined +### oas3-operation-security-defined Operation `security` values must match a scheme defined in the `components.securitySchemes` object. **Recommended:** Yes -### server-not-example.com +### oas3-server-not-example.com -Server URL should not point at `example.com`. +Server URL should not point at example.com. **Recommended:** No @@ -508,7 +510,7 @@ servers: We have example.com for documentation purposes here, but you should put in actual domains. -### server-trailing-slash +### oas3-server-trailing-slash Server URL should not have a trailing slash. @@ -532,7 +534,17 @@ servers: - url: https://example.com/api/ ``` -### valid-example +### oas3-unused-components-schema + +Potential unused reusable `schema` entry has been detected. + +_Warning:_ This rule may identify false positives when linting a specification +that acts as a library (a container storing reusable objects, leveraged by other +specifications that reference those objects). + +**Recommended:** Yes + +### oas3-valid-example Examples must be valid against their defined schema. @@ -548,4 +560,4 @@ Validate structure of OpenAPI v3 specification. Parameter objects should have a `description`. -**Recommended:** No \ No newline at end of file +**Recommended:** No diff --git a/karma-jest.ts b/karma-jest.ts index ef4a32329..3fe2bbd9f 100644 --- a/karma-jest.ts +++ b/karma-jest.ts @@ -1,8 +1,17 @@ -(global as any).jest = require('jest-mock'); -(global as any).expect = require('expect'); -(global as any).test = it; +import { Expect } from 'expect/build/types'; +import * as JestMock from 'jest-mock'; -const message = "Good try. An email has been sent to Vincenzo and Jakub, and they'll find you. :troll: ;)"; +declare var global: NodeJS.Global & { + jest: typeof JestMock; + expect: Expect; + test: jest.It; +}; + +global.jest = require('jest-mock'); +global.expect = require('expect'); +global.test = it; + +const message = () => "Good try. An email has been sent to Vincenzo and Jakub, and they'll find you. :troll: ;)"; expect.extend({ toMatchSnapshot: () => ({ pass: false, message }), @@ -11,7 +20,7 @@ expect.extend({ // @ts-ignore test.each = input => (name: string, fn: Function) => { - // very simple stub-like implementation needed by src/rulesets/oas3/__tests__/valid-example.ts and src/rulesets/__tests__/validation.test.ts + // very simple stub-like implementation needed by src/rulesets/oas/__tests__/valid-example.ts and src/rulesets/__tests__/validation.test.ts for (const value of input) { if (Array.isArray(value)) { fn(...value); diff --git a/package.json b/package.json index 750af32e6..326d6c955 100644 --- a/package.json +++ b/package.json @@ -30,23 +30,26 @@ "rulesets/*" ], "engines": { - "node": ">=8.3.0" + "node": ">=10.0" }, "scripts": { "build.binary": "pkg . --output ./binaries/spectral", + "build.clean": "rimraf ./coverage && rimraf ./dist && rimraf ./rulesets && rimraf ./__karma__/__fixtures__", "build.oas-functions": "rollup -c", "build": "tsc -p ./tsconfig.build.json", "cli": "node -r ts-node/register -r tsconfig-paths/register src/cli/index.ts", "cli:debug": "node -r ts-node/register -r tsconfig-paths/register --inspect-brk src/cli/index.ts", - "compile-rulesets": "node ./scripts/compile-rulesets.js", + "generate-assets": "node ./scripts/generate-assets.js", "inline-version": "./scripts/inline-version.js", "lint.fix": "yarn lint --fix", - "lint": "tslint 'src/**/*.ts'", + "lint": "tsc --noEmit && tslint 'src/**/*.ts'", + "copy.html-templates": "copyfiles -u 1 \"./src/formatters/html/*.html\" \"./dist/\"", "postbuild.oas-functions": "copyfiles -u 1 \"dist/rulesets/oas*/functions/*.js\" ./", - "postbuild": "yarn build.oas-functions && yarn compile-rulesets", - "prebuild": "copyfiles -u 1 \"src/rulesets/oas*/**/*.json\" dist && copyfiles -u 1 \"src/rulesets/oas*/**/*.json\" ./", + "postbuild": "yarn build.oas-functions && yarn generate-assets", + "prebuild": "yarn build.clean && copyfiles -u 1 \"src/rulesets/oas*/**/*.json\" dist && copyfiles -u 1 \"src/rulesets/oas*/**/*.json\" ./ && yarn copy.html-templates", "prebuild.binary": "yarn build", - "pretest.karma": "node ./scripts/generate-karma-fixtures.js", + "pretest.karma": "node ./scripts/generate-karma-fixtures.js && yarn pretest", + "pretest": "node ./scripts/generate-assets.js", "schema.update": "yarn typescript-json-schema --id \"http://stoplight.io/schemas/rule.schema.json\" --required tsconfig.json IRule --out ./src/meta/rule.schema.json", "test.harness": "jest -c ./jest.harness.config.js", "test.karma": "karma start", @@ -56,70 +59,75 @@ "test": "jest --silent" }, "dependencies": { - "@stoplight/json": "^3.1.1", - "@stoplight/json-ref-resolver": "^2.2.0", - "@stoplight/path": "^1.2.0", - "@stoplight/types": "^11.0.0", - "@stoplight/yaml": "^3.1.0", + "@stoplight/json": "^3.4.0", + "@stoplight/json-ref-readers": "^1.1.1", + "@stoplight/json-ref-resolver": "^3.0.6", + "@stoplight/path": "^1.3.0", + "@stoplight/types": "^11.2.0", + "@stoplight/yaml": "^3.5.0", "abort-controller": "^3.0.0", - "ajv": "^6.7", - "ajv-oai": "^1.1.1", + "ajv": "^6.10", + "ajv-oai": "^1.1.5", "better-ajv-errors": "^0.6.7", - "chalk": "^2.4.2", - "deprecated-decorator": "^0.1.6", - "fast-glob": "^3.0.4", - "jsonpath-plus": "~1.0", + "blueimp-md5": "^2.12.0", + "chalk": "^3.0.0", + "eol": "^0.9.1", + "fast-glob": "^3.1.0", + "jsonpath-plus": "~2.0", "lodash": ">=4.17.5", - "nanoid": "^2.0.3", + "nanoid": "^2.1.6", "node-fetch": "^2.6", - "proxy-agent": "^3.1.0", - "strip-ansi": "^5.2", + "proxy-agent": "^3.1.1", + "strip-ansi": "^6.0", "text-table": "^0.2", "tslib": "^1.10.0", - "typescript-json-schema": "~0.40", - "yargs": "^14.0.0" + "yargs": "^15.0.2" }, "devDependencies": { - "@commitlint/config-conventional": "^8.1.0", + "@commitlint/config-conventional": "^8.2.0", "@types/chalk": "^2.2.0", "@types/fetch-mock": "^7.3.1", - "@types/jest": "^24.0.16", - "@types/jest-when": "^2.4.0", - "@types/lodash": "^4.14.136", - "@types/nanoid": "^2.0.0", + "@types/jest": "^24.0.22", + "@types/jest-when": "^2.7.0", + "@types/lodash": "^4.14.146", + "@types/nanoid": "^2.1.0", "@types/nock": "^11.1.0", - "@types/node": "~12.7", - "@types/node-fetch": "^2.5.0", - "@types/text-table": "^0.2.0", - "@types/xml2js": "^0.4.4", - "@types/yaml": "^1.0.2", + "@types/node": "~12.12", + "@types/node-fetch": "^2.5.3", + "@types/text-table": "^0.2.1", + "@types/tmp": "^0.1.0", + "@types/xml2js": "^0.4.5", "copyfiles": "^2.1.1", - "fetch-mock": "^7.3.9", + "dependency-graph": "^0.8.0", + "fetch-mock": "^7.7.3", "glob-fs": "^0.1.7", - "husky": "^3.0.0", + "husky": "^3.0.9", "jest": "~24.9", - "jest-mock": "^24.8.0", + "jest-mock": "^24.9.0", "jest-when": "~2.7", - "karma": "^4.2.0", - "karma-chrome-launcher": "^3.0.0", + "karma": "^4.4.1", + "karma-chrome-launcher": "^3.1.0", "karma-jasmine": "^2.0.1", "karma-typescript": "^4.1.1", - "lint-staged": "^9.0.2", - "nock": "~11.4.0", + "lint-staged": "^9.4.2", + "nock": "~11.7.0", + "node-html-parser": "^1.1.16", "pkg": "^4.4.0", - "recast": "^0.18.1", - "rollup": "^1.19.4", - "rollup-plugin-commonjs": "^10.0.2", + "recast": "^0.18.5", + "rimraf": "^3.0.0", + "rollup": "^1.27.14", + "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-node-resolve": "^5.2.0", - "rollup-plugin-terser": "^5.1.1", - "rollup-plugin-typescript2": "^0.24.0", - "ts-jest": "^24.0.2", - "ts-node": "^8.1.0", - "tsconfig-paths": "^3.8.0", + "rollup-plugin-terser": "^5.1.2", + "rollup-plugin-typescript2": "^0.25.2", + "ts-jest": "^24.1.0", + "ts-node": "^8.4.1", + "tsconfig-paths": "^3.9.0", "tslint": "~5.20", - "tslint-config-stoplight": "~1.3", - "typescript": "^3.5.3", - "xml2js": "^0.4.21" + "tslint-config-stoplight": "~1.4", + "typescript": "^3.7.4", + "typescript-json-schema": "~0.41.0", + "xml2js": "^0.4.23" }, "lint-staged": { "*.{ts,tsx}": [ @@ -137,7 +145,8 @@ "./dist/**/*.js" ], "assets": [ - "./dist/**/*.json" + "./dist/**/*.json", + "./dist/cli/**/*.html" ] }, "types": "dist/index.d.ts" diff --git a/rollup.config.js b/rollup.config.js index 44db745ed..d74dac46b 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,32 +1,43 @@ import typescript from 'rollup-plugin-typescript2'; import * as path from 'path'; +import * as fs from 'fs'; import resolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; import { terser } from 'rollup-plugin-terser'; const BASE_PATH = process.cwd(); +const directory = 'dist/rulesets/oas/functions'; +const targetDir = path.join(BASE_PATH, directory); -module.exports = [ - 'oasOp2xxResponse', - 'oasOpFormDataConsumeCheck', - 'oasOpIdUnique', - 'oasOpParams', - 'oasOpSecurityDefined', - 'oasPathParam', - 'refSiblings' -].map(fn => ({ - input: path.resolve(BASE_PATH, 'dist/rulesets/oas/functions', `${fn}.js`), +const functions = []; +for (const file of fs.readdirSync(targetDir)) { + const targetFile = path.join(targetDir, file); + const stat = fs.statSync(targetFile); + if (!stat.isFile()) continue; + const ext = path.extname(targetFile); + if (ext !== '.js') continue; + + functions.push(targetFile); +} + +module.exports = functions.map(fn => ({ + input: fn, plugins: [ typescript({ tsconfig: path.join(BASE_PATH, './tsconfig.rollup.json'), include: ['dist/**/*.{ts,tsx}'], }), resolve(), - commonjs(), + commonjs({ + namedExports: { + 'node_modules/lodash/lodash.js': ['isObject', 'trimStart'], + 'node_modules/@stoplight/types/dist/index.js': ['DiagnosticSeverity'], + }, + }), terser(), ], output: { - file: path.resolve(BASE_PATH, 'dist/rulesets/oas/functions', `${fn}.js`), + file: fn, format: 'cjs', exports: 'named' }, diff --git a/scripts/compile-rulesets.js b/scripts/compile-rulesets.js deleted file mode 100755 index 9b8202dae..000000000 --- a/scripts/compile-rulesets.js +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env node -const path = require('path'); -const fs = require('fs'); -const { readRuleset } = require('../dist/rulesets'); - -const dist = path.join(__dirname, '../rulesets/precompiled/'); - -const rulesetNames = ['oas2', 'oas3', 'oas']; - -try { - fs.mkdirSync(dist); -} catch (ex) { - -} - -Promise.all( - rulesetNames.map(ruleset => readRuleset(`spectral:${ruleset}`)), -).then(rulesets => { - for (const [i, ruleset] of rulesets.entries()) { - fs.writeFileSync( - path.join(dist, `${rulesetNames[i]}.json`), - JSON.stringify(ruleset), - ); - } -}); - diff --git a/scripts/generate-assets.js b/scripts/generate-assets.js new file mode 100755 index 000000000..c910ef4a7 --- /dev/null +++ b/scripts/generate-assets.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node +/** + * This script generates a list of assets that are needed to load spectral:oas ruleset. + * It contains all OAS custom functions and *resolved* rulesets. + * The assets are stores in a single filed call assets.json in the following format: + * `: ` + * where the `require-call-path` is the path you'd normally pass to require(), i.e. `@stoplight/spectral/rulesets/oas/index.js` and `content` is the text data. + * Assets can be loaded using Spectral#registerStaticAssets statc method, i.e. `Spectral.registerStaticAssets(require('@stoplight/spectral/rulesets/assets/assets.json'))`; + * If you execute the code above, ruleset will be loaded fully offline, without a need to make any request. + */ + +const path = require('@stoplight/path'); +const fs = require('fs'); +const { promisify } = require('util'); +const { parse } = require('@stoplight/yaml'); +const { httpAndFileResolver } = require('../dist/resolvers/http-and-file'); + +const readFileAsync = promisify(fs.readFile); +const writeFileAsync = promisify(fs.writeFile); +const readdirAsync = promisify(fs.readdir); +const statAsync = promisify(fs.stat); + +const baseDir = path.join(__dirname, '../rulesets/assets/'); + +if (!fs.existsSync(baseDir)) { + fs.mkdirSync(baseDir); +} + +const target = path.join(baseDir, `assets.json`); +const assets = {}; + +(async () => { + await processDirectory(assets, path.join(__dirname, '../rulesets/oas')); + await writeFileAsync(target, JSON.stringify(assets, null, 2)); +})(); + +async function processDirectory(assets, dir) { + await Promise.all((await readdirAsync(dir)).map(async name => { + if (name === 'schemas') return; + const target = path.join(dir, name); + const stats = await statAsync(target); + if (stats.isDirectory()) { + return processDirectory(assets, target); + } else { + let content = await readFileAsync(target, 'utf8'); + if (path.extname(name) === '.json') { + content = JSON.stringify((await httpAndFileResolver.resolve(JSON.parse(content), { + dereferenceRemote: true, + dereferenceInline: false, + baseUri: target, + parseResolveResult(opts) { + opts.result = parse(opts.result); + return opts; + }, + })).result); + } + + assets[path.join('@stoplight/spectral', path.relative(path.join(__dirname, '..'), target))] = content; + } + })); +} diff --git a/scripts/generate-karma-fixtures.js b/scripts/generate-karma-fixtures.js index cafb2ece0..9bef59baa 100755 --- a/scripts/generate-karma-fixtures.js +++ b/scripts/generate-karma-fixtures.js @@ -2,16 +2,24 @@ const path = require('path'); const fs = require('fs'); -const target = path.join(__dirname, '../src/__tests__/__fixtures__/oas-functions.json'); +const baseDir = path.join(__dirname, '../__karma__/__fixtures__/'); -const fnsPath = path.join(__dirname, '../rulesets/oas/functions') +if (!fs.existsSync(baseDir)) { + fs.mkdirSync(baseDir); +} -const files = fs.readdirSync(fnsPath); +for (const spec of ['', '2', '3']) { + const target = path.join(baseDir, `oas${spec}-functions.json`); + const fnsPath = path.join(__dirname, `../rulesets/oas${spec}/functions`); + const bundledFns = {}; -const bundledFns = {}; + if (fs.existsSync(fnsPath)) { + const files = fs.readdirSync(fnsPath); -for (const file of files) { - bundledFns[file] = fs.readFileSync(path.join(fnsPath, file), 'utf-8') -} + for (const file of files) { + bundledFns[file] = fs.readFileSync(path.join(fnsPath, file), 'utf-8'); + } + } -fs.writeFileSync(target, JSON.stringify(bundledFns, null, 2)) + fs.writeFileSync(target, JSON.stringify(bundledFns, null, 2)); +} diff --git a/setupJest.ts b/setupJest.ts index 0bd6170a9..d4faf7de9 100644 --- a/setupJest.ts +++ b/setupJest.ts @@ -5,6 +5,8 @@ let accessSpy: jest.SpyInstance; const { readFile, access } = fs; +jest.setTimeout(10 * 1000); + beforeEach(() => { readFileSpy = jest.spyOn(fs, 'readFile'); accessSpy = jest.spyOn(fs, 'access'); diff --git a/setupKarma.ts b/setupKarma.ts index 6b4749432..931324082 100644 --- a/setupKarma.ts +++ b/setupKarma.ts @@ -1,12 +1,10 @@ +import * as jsonSpecv4 from 'ajv/lib/refs/json-schema-draft-04.json'; import { FetchMockSandbox } from 'fetch-mock'; const oasRuleset = JSON.parse(JSON.stringify(require('./rulesets/oas/index.json'))); -const oas2Ruleset = JSON.parse(JSON.stringify(require('./rulesets/oas2/index.json'))); -const oas2Schema = JSON.parse(JSON.stringify(require('./rulesets/oas2/schemas/main.json'))); -const oas3Ruleset = JSON.parse(JSON.stringify(require('./rulesets/oas3/index.json'))); -const oas3Schema = JSON.parse(JSON.stringify(require('./rulesets/oas3/schemas/main.json'))); - -const oasFunctions = JSON.parse(JSON.stringify(require('./src/__tests__/__fixtures__/oas-functions.json'))); +const oasFunctions = JSON.parse(JSON.stringify(require('./__karma__/__fixtures__/oas-functions.json'))); +const oas2Schema = JSON.parse(JSON.stringify(require('./rulesets/oas/schemas/schema.oas2.json'))); +const oas3Schema = JSON.parse(JSON.stringify(require('./rulesets/oas/schemas/schema.oas3.json'))); const { fetch } = window; let fetchMock: FetchMockSandbox; @@ -20,22 +18,12 @@ beforeEach(() => { body: JSON.parse(JSON.stringify(oasRuleset)), }); - fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas2/index.json', { - status: 200, - body: JSON.parse(JSON.stringify(oas2Ruleset)), - }); - - fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas3/index.json', { - status: 200, - body: JSON.parse(JSON.stringify(oas3Ruleset)), - }); - - fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas2/schemas/main.json', { + fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas/schemas/schema.oas2.json', { status: 200, body: JSON.parse(JSON.stringify(oas2Schema)), }); - fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas3/schemas/main.json', { + fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas/schemas/schema.oas3.json', { status: 200, body: JSON.parse(JSON.stringify(oas3Schema)), }); @@ -46,6 +34,11 @@ beforeEach(() => { body: fn, }); } + + fetchMock.get('http://json-schema.org/draft-04/schema', { + status: 200, + body: JSON.parse(JSON.stringify(jsonSpecv4)), + }); }); afterEach(() => { diff --git a/test-harness/scenarios/rulesets/oas-mixed-severity.json b/src/__tests__/__fixtures__/bare-oas-ruleset.json similarity index 73% rename from test-harness/scenarios/rulesets/oas-mixed-severity.json rename to src/__tests__/__fixtures__/bare-oas-ruleset.json index 244a278be..3c9801844 100644 --- a/test-harness/scenarios/rulesets/oas-mixed-severity.json +++ b/src/__tests__/__fixtures__/bare-oas-ruleset.json @@ -1,12 +1,8 @@ { - "extends": ["spectral:oas"], "rules": { - "info-contact": "error", - "operation-description": "info", "info-matches-stoplight": { "message": "Info must contain Stoplight", "given": "$.info", - "severity": "hint", "type": "style", "recommended": true, "then": { diff --git a/src/__tests__/__fixtures__/gh-658/URIError.yaml b/src/__tests__/__fixtures__/gh-658/URIError.yaml new file mode 100644 index 000000000..4d545fa39 --- /dev/null +++ b/src/__tests__/__fixtures__/gh-658/URIError.yaml @@ -0,0 +1,44 @@ +%YAML 1.2 +--- +openapi: 3.0.0 + +info: + title: Hey Mum! I'm on GitHub! + description: Resource definition. + version: 1.0.0 + +servers: + - url: https://boom.com + +paths: + "/test": + get: + summary: Dummy endpoint. + description: Cf. summary + responses: + "200": + description: All is good. + content: + application/json: + schema: + type: string + "400": + description: Bad Request. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: No Bueno. + content: + application/json: + schema: + $ref: "./lib.yaml#/components/schemas/Error" +components: + schemas: + Error: + $ref: "#/components/schemas/Baz" + Baz: + $ref: ./lib.yaml#/components/schemas/Error + Foo: + type: string diff --git a/src/__tests__/__fixtures__/gh-658/lib.yaml b/src/__tests__/__fixtures__/gh-658/lib.yaml new file mode 100644 index 000000000..be265ae0c --- /dev/null +++ b/src/__tests__/__fixtures__/gh-658/lib.yaml @@ -0,0 +1,24 @@ +%YAML 1.2 +--- +openapi: 3.0.0 + +info: + title: Library + description: Collection of reusable standard definitions. + version: 2.0.0 +paths: {} +components: + schemas: + Error: + type: object + properties: + error: + type: string + maxLength: 1 + error_description: + type: string + maxLength: 1 + status_code: + type: string + test: + $ref: ./URIError.yaml#/components/schemas/Foo diff --git a/src/__tests__/__fixtures__/schemas/name.json b/src/__tests__/__fixtures__/schemas/name.json new file mode 100644 index 000000000..7b4d43351 --- /dev/null +++ b/src/__tests__/__fixtures__/schemas/name.json @@ -0,0 +1,6 @@ +{ + "type": "string", + "maxLength": { + "$ref": "./broken-length.json" + } +} diff --git a/src/__tests__/__fixtures__/schemas/user.json b/src/__tests__/__fixtures__/schemas/user.json new file mode 100644 index 000000000..2737ab37c --- /dev/null +++ b/src/__tests__/__fixtures__/schemas/user.json @@ -0,0 +1,8 @@ +{ + "name": { + "$ref": "./name.json" + }, + "age": { + "$ref": "./broken-age.yaml" + } +} diff --git a/src/__tests__/linter.jest.test.ts b/src/__tests__/linter.jest.test.ts index cbc744496..89520bc83 100644 --- a/src/__tests__/linter.jest.test.ts +++ b/src/__tests__/linter.jest.test.ts @@ -1,7 +1,6 @@ -import * as path from '@stoplight/path'; -import { join } from '@stoplight/path'; +import { normalize } from '@stoplight/path'; import { DiagnosticSeverity } from '@stoplight/types'; -import { isOpenApiv2, isOpenApiv3 } from '../formats'; +import * as path from 'path'; import { httpAndFileResolver } from '../resolvers/http-and-file'; import { Spectral } from '../spectral'; @@ -13,8 +12,6 @@ describe('Linter', () => { beforeEach(() => { spectral = new Spectral(); - spectral.registerFormat('oas3', isOpenApiv3); - spectral.registerFormat('oas2', isOpenApiv2); }); it('should make use of custom functions', async () => { @@ -67,18 +64,52 @@ describe('Linter', () => { it('should respect the scope of defined functions (ruleset-based)', async () => { await spectral.loadRuleset(customDirectoryFunctionsRuleset); expect(await spectral.run({})).toEqual([ - expect.objectContaining({ - code: 'has-info-property', - message: 'info property is missing', - }), expect.objectContaining({ code: 'has-field-property', message: 'Object does not have field property', }), + expect.objectContaining({ + code: 'has-info-property', + message: 'info property is missing', + }), ]); }); - describe('evaluate {{value}} in validation messages', () => { + it('should report resolving errors for correct files', async () => { + spectral = new Spectral({ resolver: httpAndFileResolver }); + + const documentUri = path.join(__dirname, './__fixtures__/schemas/doc.json'); + const result = await spectral.run( + { + $ref: './user.json', + }, + { + ignoreUnknownFormat: true, + resolve: { + documentUri, + }, + }, + ); + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: 'invalid-ref', + message: `ENOENT: no such file or directory, open '${path.join(documentUri, '../broken-age.yaml')}'`, + path: ['age', '$ref'], + source: normalize(path.join(documentUri, '../user.json')), + }), + expect.objectContaining({ + code: 'invalid-ref', + message: `ENOENT: no such file or directory, open '${path.join(documentUri, '../broken-length.json')}'`, + path: ['maxLength', '$ref'], + source: normalize(path.join(documentUri, '../name.json')), + }), + ]), + ); + }); + + describe('evaluate "value" in validation messages', () => { test('should print correct values for referenced files', async () => { spectral = new Spectral({ resolver: httpAndFileResolver }); @@ -87,7 +118,7 @@ describe('Linter', () => { severity: DiagnosticSeverity.Error, recommended: true, description: 'Should be falsy', - message: 'Value {{value}} should be falsy', + message: 'Value {{value|to-string}} should be falsy', given: '$..empty', then: { function: 'falsy', @@ -114,7 +145,7 @@ describe('Linter', () => { }, { resolve: { - documentUri: join(__dirname, 'foo.json'), + documentUri: path.join(__dirname, 'foo.json'), }, }, ), @@ -123,17 +154,17 @@ describe('Linter', () => { expect.objectContaining({ code: 'empty-is-falsy', message: 'Value "https://example.com" should be falsy', - path: ['empty'], + path: ['info', 'contact', 'url'], }), expect.objectContaining({ code: 'empty-is-falsy', message: 'Value Array[] should be falsy', - path: ['bar', 'empty'], + path: ['servers'], }), expect.objectContaining({ code: 'empty-is-falsy', message: 'Value Object{} should be falsy', - path: ['foo', 'empty'], + path: ['info'], }), ]), ); diff --git a/src/__tests__/linter.test.ts b/src/__tests__/linter.test.ts index 36b6c651e..d3111c51f 100644 --- a/src/__tests__/linter.test.ts +++ b/src/__tests__/linter.test.ts @@ -41,23 +41,21 @@ describe('linter', () => { beforeEach(() => { spectral = new Spectral(); - spectral.registerFormat('oas3', isOpenApiv3); spectral.registerFormat('oas2', isOpenApiv2); + spectral.registerFormat('oas3', isOpenApiv3); }); - test('should not lint if passed in value is not an object', async () => { + test('should not throw if passed in value is not an object', () => { const fakeLintingFunction = jest.fn(); spectral.setFunctions({ [fnName]: fakeLintingFunction, }); spectral.setRules(rules); - const result = await spectral.run('123'); - - expect(result).toHaveLength(0); + return expect(spectral.run('123')).resolves.toBeTruthy(); }); - test('should return all properties', async () => { + test('should return all properties matching 4xx response code', async () => { const message = '4xx responses require a description'; spectral.setFunctions({ @@ -76,11 +74,7 @@ describe('linter', () => { spectral.setRules({ rule1: { - given: '$.responses[*]', - when: { - field: '@key', - pattern: '^4.*', - }, + given: '$.responses.[?(@property >= 400 && @property < 500)]', then: { field: 'description', function: 'func1', @@ -88,33 +82,38 @@ describe('linter', () => { }, }); - const result = await spectral.run({ - responses: { - '200': { - name: 'ok', - }, - '404': { - name: 'not found', + const result = await spectral.run( + { + responses: { + '200': { + name: 'ok', + }, + '404': { + name: 'not found', + }, }, }, - }); + { ignoreUnknownFormat: true }, + ); - expect(result[0]).toMatchObject({ - code: 'rule1', - message, - severity: DiagnosticSeverity.Warning, - path: ['responses', '404'], - range: { - end: { - line: 6, - character: 25, - }, - start: { - character: 10, - line: 5, + expect(result).toEqual([ + expect.objectContaining({ + code: 'rule1', + message, + severity: DiagnosticSeverity.Warning, + path: ['responses', '404'], + range: { + end: { + line: 6, + character: 25, + }, + start: { + character: 10, + line: 5, + }, }, - }, - }); + }), + ]); }); test('should support rule overriding severity', async () => { @@ -138,24 +137,35 @@ describe('linter', () => { }, }); - const result = await spectral.run({ - x: true, - }); + const result = await spectral.run( + { + x: true, + }, + { ignoreUnknownFormat: true }, + ); expect(result[0]).toHaveProperty('severity', DiagnosticSeverity.Hint); }); test('should not report anything for disabled rules', async () => { - await spectral.loadRuleset('spectral:oas3'); - const { rules: oas3Rules } = await readRuleset('spectral:oas3'); - spectral.setRules(mergeRules(oas3Rules, { - 'valid-example-in-schemas': 'off', - 'model-description': -1, - }) as RuleCollection); + await spectral.loadRuleset('spectral:oas'); + const { rules: oasRules } = await readRuleset('spectral:oas'); + spectral.setRules( + mergeRules(oasRules, { + 'oas3-valid-schema-example': 'off', + 'operation-2xx-response': -1, + 'openapi-tags': 'off', + }) as RuleCollection, + ); const result = await spectral.run(invalidSchema); expect(result).toEqual([ + expect.objectContaining({ + code: 'oas3-schema', + message: `\`200\` property should have required property \`$ref\``, + path: ['paths', '/pets', 'get', 'responses', '200'], + }), expect.objectContaining({ code: 'invalid-ref', }), @@ -163,15 +173,20 @@ describe('linter', () => { code: 'invalid-ref', }), expect.objectContaining({ - code: 'oas3-schema', - message: "/paths//pets/get/responses/200 should have required property '$ref'", - path: ['paths', '/pets', 'get', 'responses', '200'], + code: 'oas3-unused-components-schema', + message: 'Potentially unused components schema has been detected.', + path: ['components', 'schemas', 'Pets'], + }), + expect.objectContaining({ + code: 'oas3-unused-components-schema', + message: 'Potentially unused components schema has been detected.', + path: ['components', 'schemas', 'foo'], }), ]); }); test('should output unescaped json paths', async () => { - await spectral.loadRuleset('spectral:oas3'); + await spectral.loadRuleset('spectral:oas'); const result = await spectral.run(invalidSchema); @@ -179,7 +194,7 @@ describe('linter', () => { expect.arrayContaining([ expect.objectContaining({ code: 'oas3-schema', - message: "/paths//pets/get/responses/200 should have required property '$ref'", + message: `\`200\` property should have required property \`$ref\``, path: ['paths', '/pets', 'get', 'responses', '200'], }), ]), @@ -204,10 +219,13 @@ describe('linter', () => { }, }); - const result = await spectral.run({ - x: false, - y: '', - }); + const result = await spectral.run( + { + x: false, + y: '', + }, + { ignoreUnknownFormat: true }, + ); expect(result).toEqual([ expect.objectContaining({ @@ -327,6 +345,10 @@ describe('linter', () => { }); expect(result).toEqual([ + expect.objectContaining({ + code: 'unrecognized-format', + message: 'The provided document does not match any of the registered formats [oas2, oas3]', + }), expect.objectContaining({ code: 'rule3', }), @@ -441,14 +463,70 @@ describe('linter', () => { }); expect(result).toEqual([ + { + message: 'The provided document does not match any of the registered formats [oas2, oas3, foo-bar]', + path: [], + range: expect.any(Object), + severity: DiagnosticSeverity.Warning, + code: 'unrecognized-format', + }, expect.objectContaining({ code: 'rule3', }), ]); }); + test('given a string input, should warn about unmatched formats', async () => { + const result = await spectral.run('test'); + + expect(result).toEqual([ + { + code: 'unrecognized-format', + message: 'The provided document does not match any of the registered formats [oas2, oas3]', + path: [], + range: { + end: { + character: 4, + line: 0, + }, + start: { + character: 0, + line: 0, + }, + }, + severity: DiagnosticSeverity.Warning, + }, + ]); + }); + + test('given ignoreUnknownFormat, should not warn about unmatched formats', async () => { + spectral.registerFormat('foo-bar', obj => typeof obj === 'object' && obj !== null && 'foo-bar' in obj); + + spectral.setRules({ + rule1: { + given: '$.x', + formats: ['foo-bar'], + severity: 'error', + then: { + function: 'truthy', + }, + }, + }); + + const result = await spectral.run( + { + 'bar-foo': true, + x: true, + y: '', + }, + { ignoreUnknownFormat: true }, + ); + + expect(result).toEqual([]); + }); + test('should include parser diagnostics', async () => { - await spectral.loadRuleset('spectral:oas2'); + await spectral.loadRuleset('spectral:oas'); const responses = `openapi: 2.0.0 responses:: !!foo @@ -501,7 +579,9 @@ responses:: !!foo }); test('should report a valid line number for json paths containing escaped slashes', async () => { - await spectral.loadRuleset('spectral:oas3'); + spectral.registerFormat('oas2', isOpenApiv2); + spectral.registerFormat('oas3', isOpenApiv3); + await spectral.loadRuleset('spectral:oas'); const result = await spectral.run(studioFixture); @@ -526,11 +606,24 @@ responses:: !!foo }); test('should remove all redundant ajv errors', async () => { - await spectral.loadRuleset('spectral:oas3'); + spectral.registerFormat('oas2', isOpenApiv2); + spectral.registerFormat('oas3', isOpenApiv3); + await spectral.loadRuleset('spectral:oas'); const result = await spectral.run(invalidSchema); expect(result).toEqual([ + expect.objectContaining({ + code: 'openapi-tags', + }), + expect.objectContaining({ + code: 'operation-tag-defined', + }), + expect.objectContaining({ + code: 'oas3-schema', + message: `\`200\` property should have required property \`$ref\``, + path: ['paths', '/pets', 'get', 'responses', '200'], + }), expect.objectContaining({ code: 'invalid-ref', }), @@ -538,28 +631,35 @@ responses:: !!foo code: 'invalid-ref', }), expect.objectContaining({ - code: 'valid-example-in-schemas', - message: '"foo.example" property type should be number', - path: ['components', 'schemas', 'foo', 'example'], + code: 'oas3-unused-components-schema', + message: 'Potentially unused components schema has been detected.', + path: ['components', 'schemas', 'Pets'], }), expect.objectContaining({ - code: 'oas3-schema', - message: "/paths//pets/get/responses/200 should have required property '$ref'", - path: ['paths', '/pets', 'get', 'responses', '200'], + code: 'oas3-unused-components-schema', + message: 'Potentially unused components schema has been detected.', + path: ['components', 'schemas', 'foo'], + }), + expect.objectContaining({ + code: 'oas3-valid-schema-example', + message: '`example` property type should be number', + path: ['components', 'schemas', 'foo', 'example'], }), ]); }); test('should report invalid schema $refs', async () => { - await spectral.loadRuleset('spectral:oas2'); + spectral.registerFormat('oas2', isOpenApiv2); + spectral.registerFormat('oas3', isOpenApiv3); + await spectral.loadRuleset('spectral:oas'); const result = await spectral.run(todosInvalid); expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ - code: 'valid-example-in-parameters', - message: '"schema.example" property can\'t resolve reference #/parameters/missing from id #', + code: 'oas2-valid-parameter-example', + message: "can't resolve reference #/parameters/missing from id #", path: ['paths', '/todos/{todoId}', 'put', 'parameters', '1', 'schema', 'example'], }), ]), @@ -567,7 +667,7 @@ responses:: !!foo }); test('should report invalid $refs', async () => { - await spectral.loadRuleset('spectral:oas3'); + await spectral.loadRuleset('spectral:oas'); const result = await spectral.run(invalidSchema); @@ -590,7 +690,13 @@ responses:: !!foo }); test('should support YAML merge keys', async () => { - await spectral.loadRuleset('spectral:oas3'); + await spectral.loadRuleset('spectral:oas'); + spectral.setRules({ + 'operation-tag-defined': { + ...spectral.rules['operation-tag-defined'], + severity: 'off', + }, + }); const result = await spectral.run(petstoreMergeKeys); @@ -599,10 +705,13 @@ responses:: !!foo describe('reports duplicated properties for', () => { test('JSON format', async () => { - const result = await spectral.run({ - parsed: parseWithPointers('{"foo":true,"foo":false}', { ignoreDuplicateKeys: false }), - getLocationForJsonPath, - }); + const result = await spectral.run( + { + parsed: parseWithPointers('{"foo":true,"foo":false}', { ignoreDuplicateKeys: false }), + getLocationForJsonPath, + }, + { ignoreUnknownFormat: true }, + ); expect(result).toEqual([ { @@ -625,7 +734,7 @@ responses:: !!foo }); test('YAML format', async () => { - const result = await spectral.run(`foo: bar\nfoo: baz`); + const result = await spectral.run(`foo: bar\nfoo: baz`, { ignoreUnknownFormat: true }); expect(result).toEqual([ { @@ -648,6 +757,36 @@ responses:: !!foo }); }); + test('should report invalid YAML mapping keys', async () => { + const results = await spectral.run( + `responses: + 200: + description: '' + '400': + description: ''`, + { ignoreUnknownFormat: true }, + ); + + expect(results).toEqual([ + { + code: 'parser', + message: 'Mapping key must be a string scalar rather than number', + path: ['responses', '200'], + range: { + end: { + character: 5, + line: 1, + }, + start: { + character: 2, + line: 1, + }, + }, + severity: DiagnosticSeverity.Error, + }, + ]); + }); + describe('functional tests for the given property', () => { let fakeLintingFunction: any; @@ -667,133 +806,44 @@ responses:: !!foo expect(fakeLintingFunction.mock.calls[0][2].given).toEqual(['responses']); expect(fakeLintingFunction.mock.calls[0][3].given).toEqual(target.responses); }); - }); - describe('when given path is not set', () => { - test('should pass through root object', async () => { + test('given array of paths, should pass each given path through to lint function', async () => { spectral.setRules({ example: { message: '', - given: '$', + given: ['$.responses', '$..200'], then: { function: fnName, }, }, }); - await spectral.run(target); - - expect(fakeLintingFunction).toHaveBeenCalledTimes(1); - expect(fakeLintingFunction.mock.calls[0][2].given).toEqual([]); - expect(fakeLintingFunction.mock.calls[0][3].given).toEqual(target); - }); - }); - }); - - describe('functional tests for the when statement', () => { - let fakeLintingFunction: any; - beforeEach(() => { - fakeLintingFunction = jest.fn(); - spectral.setFunctions({ - [fnName]: fakeLintingFunction, - }); - spectral.setRules(rules); - }); - - describe('given no when', () => { - test('should simply lint anything it matches in the given', async () => { await spectral.run(target); - expect(fakeLintingFunction).toHaveBeenCalledTimes(1); - expect(fakeLintingFunction.mock.calls[0][0]).toEqual(target.responses); - }); - }); - - describe('given when with no pattern and regular field', () => { - test('should call linter if when field exists', async () => { - spectral.mergeRules({ - example: { - when: { - field: '200.description', - }, - }, - }); - await spectral.run(target); - - expect(fakeLintingFunction).toHaveBeenCalledTimes(1); - expect(fakeLintingFunction.mock.calls[0][0]).toEqual({ - '200': { description: 'a' }, - '201': { description: 'b' }, - '300': { description: 'c' }, - }); - }); - - test('should not call linter if when field not exist', async () => { - spectral.mergeRules({ - example: { - when: { - field: '302.description', - }, - }, - }); - await spectral.run(target); - - expect(fakeLintingFunction).toHaveBeenCalledTimes(0); + expect(fakeLintingFunction).toHaveBeenCalledTimes(2); + expect(fakeLintingFunction.mock.calls[0][2].given).toEqual(['responses']); + expect(fakeLintingFunction.mock.calls[0][3].given).toEqual(target.responses); + expect(fakeLintingFunction.mock.calls[1][2].given).toEqual(['responses', '200']); + expect(fakeLintingFunction.mock.calls[1][3].given).toEqual(target.responses['200']); }); }); - describe('given "when" with a pattern and regular field', () => { - test('should NOT lint if pattern does not match', async () => { - spectral.mergeRules({ - example: { - when: { - field: '200.description', - pattern: 'X', - }, - }, - }); - await spectral.run(target); - - expect(fakeLintingFunction).toHaveBeenCalledTimes(0); - }); - - test('should lint if pattern does match', async () => { - spectral.mergeRules({ + describe('when given path is not set', () => { + test('should pass through root object', async () => { + spectral.setRules({ example: { - when: { - field: '200.description', - pattern: 'a', + message: '', + given: '$', + then: { + function: fnName, }, }, }); await spectral.run(target); expect(fakeLintingFunction).toHaveBeenCalledTimes(1); - expect(fakeLintingFunction.mock.calls[0][0]).toEqual({ - '200': { description: 'a' }, - '201': { description: 'b' }, - '300': { description: 'c' }, - }); - }); - }); - - describe('given "when" with a pattern and @key field', () => { - test('should lint ONLY part of object that matches pattern', async () => { - spectral.mergeRules({ - example: { - given: '$.responses[*]', - when: { - field: '@key', - pattern: '2..', - }, - }, - }); - await spectral.run(target); - - expect(fakeLintingFunction).toHaveBeenCalledTimes(2); - expect(fakeLintingFunction.mock.calls[0][0]).toEqual({ - description: 'a', - }); + expect(fakeLintingFunction.mock.calls[0][2].given).toEqual([]); + expect(fakeLintingFunction.mock.calls[0][3].given).toEqual(target); }); }); }); @@ -881,7 +931,7 @@ responses:: !!foo severity: DiagnosticSeverity.Error, recommended: true, description: 'A parameter in the header should be written in kebab-case', - message: '{{value}} is not kebab-cased: {{error}}', + message: '{{value|to-string}} is not kebab-cased: {{error}}', given: "$..parameters[?(@.in === 'header')]", then: { field: 'name', @@ -932,7 +982,7 @@ responses:: !!foo severity: DiagnosticSeverity.Error, recommended: true, description: 'Should be falsy', - message: 'Value {{value}} should be falsy', + message: 'Value {{value|to-string}} should be falsy', given: '$..empty', then: { function: 'falsy', @@ -970,7 +1020,9 @@ responses:: !!foo }); test('should evaluate {{path}} in validation messages', async () => { - await spectral.loadRuleset('spectral:oas3'); + spectral.registerFormat('oas2', isOpenApiv2); + spectral.registerFormat('oas3', isOpenApiv3); + await spectral.loadRuleset('spectral:oas'); spectral.setRules({ 'oas3-schema': { ...spectral.rules['oas3-schema'], @@ -990,6 +1042,8 @@ responses:: !!foo }); test('should report ref siblings', async () => { + spectral.registerFormat('oas2', isOpenApiv2); + spectral.registerFormat('oas3', isOpenApiv3); await spectral.loadRuleset('spectral:oas'); const results = await spectral.run({ diff --git a/src/__tests__/spectral.jest.test.ts b/src/__tests__/spectral.jest.test.ts index 73bc3e8ca..2398c2a49 100644 --- a/src/__tests__/spectral.jest.test.ts +++ b/src/__tests__/spectral.jest.test.ts @@ -5,9 +5,10 @@ import * as nock from 'nock'; import * as path from 'path'; import { isOpenApiv2 } from '../formats'; import { httpAndFileResolver } from '../resolvers/http-and-file'; -import { Spectral } from '../spectral'; +import { IRunRule, Spectral } from '../spectral'; const oasRuleset = require('../rulesets/oas/index.json'); +const oasRulesetRules: Dictionary = oasRuleset.rules; const customOASRuleset = require('./__fixtures__/custom-oas-ruleset.json'); describe('Spectral', () => { @@ -22,7 +23,7 @@ describe('Spectral', () => { expect(s.rules).toEqual( expect.objectContaining({ - ...[...Object.entries(oasRuleset.rules)].reduce>((oasRules, [name, rule]) => { + ...[...Object.entries(oasRulesetRules)].reduce>((oasRules, [name, rule]) => { oasRules[name] = { name, ...rule, @@ -71,13 +72,32 @@ describe('Spectral', () => { 'info-matches-stoplight': { ...ruleset.rules['info-matches-stoplight'], name: 'info-matches-stoplight', - severity: -1, + severity: DiagnosticSeverity.Warning, }, }); }); }); - test('reports issues for correct files with correct ranges and paths', async () => { + test('should support combining built-in ruleset with a custom one', async () => { + const s = new Spectral(); + await s.loadRuleset(['spectral:oas', path.join(__dirname, './__fixtures__/bare-oas-ruleset.json')]); + + expect(Object.values(s.rules)).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'oas2-schema', + }), + expect.objectContaining({ + name: 'oas3-schema', + }), + expect.objectContaining({ + name: 'info-matches-stoplight', + }), + ]), + ); + }); + + test('should report issues for correct files with correct ranges and paths', async () => { const documentUri = path.join(__dirname, './__fixtures__/document-with-external-refs.oas2.json'); const spectral = new Spectral({ resolver: httpAndFileResolver }); await spectral.loadRuleset('spectral:oas'); @@ -85,6 +105,7 @@ describe('Spectral', () => { const parsed = { parsed: parseWithPointers(fs.readFileSync(documentUri, 'utf8')), getLocationForJsonPath, + source: documentUri, }; const results = await spectral.run(parsed, { @@ -108,11 +129,11 @@ describe('Spectral', () => { line: 16, }, }, - source: undefined, + source: documentUri, }), expect.objectContaining({ code: 'oas2-schema', - path: ['paths', '/todos/{todoId}', 'get', 'responses', '200', 'schema'], + path: [], range: { end: { character: 1, @@ -138,7 +159,7 @@ describe('Spectral', () => { line: 10, }, }, - source: undefined, + source: documentUri, }), expect.objectContaining({ code: 'info-contact', @@ -153,7 +174,7 @@ describe('Spectral', () => { line: 2, }, }, - source: undefined, + source: documentUri, }), expect.objectContaining({ code: 'operation-description', @@ -168,9 +189,78 @@ describe('Spectral', () => { line: 11, }, }, - source: undefined, + source: documentUri, }), ]), ); }); + + test('should recognize the source of remote $refs, and de-dupe results by fingerprint', async () => { + const s = new Spectral({ resolver: httpAndFileResolver }); + const documentUri = path.join(__dirname, './__fixtures__/gh-658/URIError.yaml'); + + s.setRules({ + 'schema-strings-maxLength': { + severity: DiagnosticSeverity.Warning, + recommended: true, + message: "String typed properties MUST be further described using 'maxLength'. Error: {{error}}", + given: "$..[?(@.type === 'string')]", + then: { + field: 'maxLength', + function: 'truthy', + }, + }, + }); + + const results = await s.run(fs.readFileSync(documentUri, 'utf8'), { resolve: { documentUri } }); + + expect(results.length).toEqual(3); + + return expect(results).toEqual([ + expect.objectContaining({ + path: ['components', 'schemas', 'Error', 'properties', 'status_code'], + source: expect.stringContaining('/src/__tests__/__fixtures__/gh-658/lib.yaml'), + range: { + end: { + character: 22, + line: 21, + }, + start: { + character: 20, + line: 20, + }, + }, + }), + + expect.objectContaining({ + path: ['paths', '/test', 'get', 'responses', '200', 'content', 'application/json', 'schema'], + source: expect.stringContaining('/src/__tests__/__fixtures__/gh-658/URIError.yaml'), + range: { + end: { + character: 28, + line: 23, + }, + start: { + character: 21, + line: 22, + }, + }, + }), + + expect.objectContaining({ + path: ['components', 'schemas', 'Foo'], + source: expect.stringContaining('/src/__tests__/__fixtures__/gh-658/URIError.yaml'), + range: { + end: { + character: 18, + line: 43, + }, + start: { + character: 8, + line: 42, + }, + }, + }), + ]); + }); }); diff --git a/src/__tests__/spectral.karma.test.ts b/src/__tests__/spectral.karma.test.ts index 89489da0e..94fcc7b17 100644 --- a/src/__tests__/spectral.karma.test.ts +++ b/src/__tests__/spectral.karma.test.ts @@ -1,3 +1,4 @@ +import { DiagnosticSeverity } from '@stoplight/types/dist'; import { FetchMockSandbox } from 'fetch-mock'; import { Spectral } from '../spectral'; @@ -44,7 +45,7 @@ describe('Spectral', () => { 'info-matches-stoplight': { ...ruleset.rules['info-matches-stoplight'], name: 'info-matches-stoplight', - severity: -1, + severity: DiagnosticSeverity.Warning, }, }); }); diff --git a/src/__tests__/spectral.test.ts b/src/__tests__/spectral.test.ts index 1b29053db..c96f31909 100644 --- a/src/__tests__/spectral.test.ts +++ b/src/__tests__/spectral.test.ts @@ -1,50 +1,44 @@ import { getLocationForJsonPath, parseWithPointers } from '@stoplight/json'; +import { IGraphNodeData } from '@stoplight/json-ref-resolver/types'; import { DiagnosticSeverity, Dictionary } from '@stoplight/types'; +import { DepGraph } from 'dependency-graph'; import { isParsedResult, Spectral } from '../spectral'; -import { IParsedResult, IResolver, RuleFunction } from '../types'; +import { IParsedResult, IResolver, IRunRule, RuleFunction } from '../types'; const merge = require('lodash/merge'); const oasRuleset = JSON.parse(JSON.stringify(require('../rulesets/oas/index.json'))); -const oas2Ruleset = JSON.parse(JSON.stringify(require('../rulesets/oas2/index.json'))); -const oas3Ruleset = JSON.parse(JSON.stringify(require('../rulesets/oas3/index.json'))); +const oasRulesetRules: Dictionary = oasRuleset.rules; describe('spectral', () => { describe('loadRuleset', () => { test('should support loading built-in rulesets', async () => { const s = new Spectral(); - await s.loadRuleset('spectral:oas2'); + await s.loadRuleset('spectral:oas'); expect(s.rules).toEqual( expect.objectContaining( - [...Object.entries(oasRuleset.rules), ...Object.entries(oas2Ruleset.rules)].reduce>( - (oasRules, [name, rule]) => { - oasRules[name] = { - name, - ...rule, - formats: expect.arrayContaining([expect.any(String)]), - severity: expect.any(Number), - then: expect.any(Object), - }; - - return oasRules; - }, - {}, - ), + Object.entries(oasRulesetRules).reduce>((oasRules, [name, rule]) => { + oasRules[name] = { + name, + ...rule, + formats: expect.arrayContaining([expect.any(String)]), + severity: expect.any(Number), + then: expect.any(Object), + }; + + return oasRules; + }, {}), ), ); }); - test('should support loading multiple built-in rulesets', async () => { + test('should support loading multiple times the built-in ruleset', async () => { const s = new Spectral(); - await s.loadRuleset(['spectral:oas2', 'spectral:oas3']); + await s.loadRuleset(['spectral:oas', 'spectral:oas']); expect(s.rules).toEqual( - [ - ...Object.entries(oasRuleset.rules), - ...Object.entries(oas2Ruleset.rules), - ...Object.entries(oas3Ruleset.rules), - ].reduce>((oasRules, [name, rule]) => { + Object.entries(oasRulesetRules).reduce>((oasRules, [name, rule]) => { oasRules[name] = { name, ...rule, @@ -63,7 +57,6 @@ describe('spectral', () => { test('should not mutate the passing in rules object', () => { const givenCustomRuleSet = { rule1: { - summary: '', given: '$', then: { function: RuleFunction.TRUTHY, @@ -131,6 +124,7 @@ describe('spectral', () => { resolve: jest.fn(async () => ({ result: {}, refMap: {}, + graph: new DepGraph(), errors: [], })), }; @@ -160,6 +154,7 @@ describe('spectral', () => { }, }, refMap: {}, + graph: new DepGraph(), errors: [], })), }; @@ -193,11 +188,11 @@ describe('spectral', () => { path: ['foo', 'bar', 'baz'], range: { end: { - character: 0, + character: 12, line: 0, }, start: { - character: 0, + character: 7, line: 0, }, }, @@ -255,11 +250,11 @@ describe('spectral', () => { }, }); - return expect(s.run(parsedResult)).resolves.toEqual([ + return expect(s.run(parsedResult, { resolve: { documentUri: source } })).resolves.toEqual([ { code: 'pagination-responses-have-x-next-token', message: 'All collection endpoints have the X-Next-Token parameter in responses', - path: ['paths', '/agreements', 'get', 'responses', '200', 'headers'], + path: ['responses', 'GetAgreementsOk', 'headers'], range: expect.any(Object), severity: DiagnosticSeverity.Error, source, diff --git a/src/assets.ts b/src/assets.ts new file mode 100644 index 000000000..7e9d8543f --- /dev/null +++ b/src/assets.ts @@ -0,0 +1,11 @@ +import { Dictionary } from '@stoplight/types'; + +function resolveSpectralRuleset(ruleset: string) { + return `@stoplight/spectral/rulesets/${ruleset}/index.json`; +} + +export const RESOLVE_ALIASES: Dictionary = { + 'spectral:oas': resolveSpectralRuleset('oas'), +}; + +export const STATIC_ASSETS: Dictionary = {}; diff --git a/src/cli/commands/__tests__/lint.test.ts b/src/cli/commands/__tests__/lint.test.ts index 044d5b112..15cf7d4f2 100644 --- a/src/cli/commands/__tests__/lint.test.ts +++ b/src/cli/commands/__tests__/lint.test.ts @@ -20,6 +20,7 @@ function run(command: string) { describe('lint', () => { let errorSpy: jest.SpyInstance; + const { isTTY } = process.stdin; const results: IRuleResult[] = [ { @@ -55,13 +56,31 @@ describe('lint', () => { afterEach(() => { errorSpy.mockRestore(); + process.stdin.isTTY = isTTY; }); - it('shows help when no document argument is passed', async () => { + it('shows help when no document and no STDIN are present', async () => { + process.stdin.isTTY = true; const output = await run('lint'); expect(output).toContain('documents Location of JSON/YAML documents'); }); + describe('when STDIN is present', () => { + it('does not show help when documents are missing', async () => { + const output = await run('lint'); + expect(output).not.toContain('documents Location of JSON/YAML documents'); + }); + + it('calls with lint with STDIN file descriptor', async () => { + await run('lint'); + expect(lint).toBeCalledWith([0], { + encoding: 'utf8', + format: 'stylish', + ignoreUnknownFormat: false, + }); + }); + }); + it('shows help when invalid arguments are passed', async () => { const output = await run('lint --foo'); expect(output).toContain('documents Location of JSON/YAML documents. Can be either a file, a glob or'); @@ -73,6 +92,7 @@ describe('lint', () => { expect(lint).toBeCalledWith([doc], { encoding: 'utf8', format: 'stylish', + ignoreUnknownFormat: false, }); }); @@ -82,6 +102,7 @@ describe('lint', () => { expect(lint).toBeCalledWith([doc], { encoding: 'utf16', format: 'stylish', + ignoreUnknownFormat: false, }); }); @@ -91,6 +112,7 @@ describe('lint', () => { expect(lint).toBeCalledWith([doc], { encoding: 'utf16', format: 'json', + ignoreUnknownFormat: false, }); }); @@ -147,6 +169,16 @@ describe('lint', () => { skipRule: ['foo', 'bar'], encoding: 'utf8', format: 'stylish', + ignoreUnknownFormat: false, + }); + }); + + it('passes ignore-unknown-format to lint', async () => { + await run('lint --ignore-unknown-format ./__fixtures__/empty-oas2-document.json'); + expect(lint).toHaveBeenCalledWith([expect.any(String)], { + encoding: 'utf8', + format: 'stylish', + ignoreUnknownFormat: true, }); }); @@ -156,7 +188,7 @@ describe('lint', () => { ); }); - it('errors upon exception', async () => { + it('prints error message upon exception', async () => { const error = new Error('Failure'); (lint as jest.Mock).mockReset(); (lint as jest.Mock).mockReturnValueOnce({ @@ -171,6 +203,6 @@ describe('lint', () => { }); await run(`lint -o foo.json ./__fixtures__/empty-oas2-document.json`); - expect(errorSpy).toBeCalledWith(error); + expect(errorSpy).toBeCalledWith('Failure'); }); }); diff --git a/src/cli/commands/lint.ts b/src/cli/commands/lint.ts index 3cedc9248..95355e789 100644 --- a/src/cli/commands/lint.ts +++ b/src/cli/commands/lint.ts @@ -1,7 +1,8 @@ import { Dictionary } from '@stoplight/types'; +import { pick } from 'lodash'; +import { ReadStream } from 'tty'; import { CommandModule, showHelp } from 'yargs'; -import { pick } from 'lodash'; import { getDiagnosticSeverity } from '../../rulesets/severity'; import { IRuleResult } from '../../types'; import { FailSeverity, ILintConfig, OutputFormat } from '../../types/config'; @@ -14,13 +15,28 @@ const formatOptions = Object.values(OutputFormat); const lintCommand: CommandModule = { describe: 'lint JSON/YAML documents from files or URLs', - command: 'lint ', + command: 'lint [documents..]', builder: yargs => yargs + .strict() .positional('documents', { description: 'Location of JSON/YAML documents. Can be either a file, a glob or fetchable resource(s) on the web.', type: 'string', + coerce(values) { + if (values.length > 0) { + return values; + } + + // https://stackoverflow.com/questions/39801643/detect-if-node-receives-stdin + // https://twitter.com/MylesBorins/status/782009479382626304 + // https://nodejs.org/dist/latest/docs/api/tty.html#tty_readstream_istty + if (process.stdin.isTTY) { + return []; + } + + return [(process.stdin as ReadStream & { fd: 0 }).fd]; + }, }) .fail(() => { showHelp(); @@ -30,6 +46,10 @@ const lintCommand: CommandModule = { return false; } + if (!Array.isArray(argv.documents) || argv.documents.length === 0) { + return false; + } + return true; }) .options({ @@ -51,6 +71,10 @@ const lintCommand: CommandModule = { description: 'output to a file instead of stdout', type: 'string', }, + resolver: { + description: 'path to custom json-ref-resolver instance', + type: 'string', + }, ruleset: { alias: 'r', description: 'path/URL to a ruleset file', @@ -67,7 +91,7 @@ const lintCommand: CommandModule = { alias: 'F', description: 'results of this level or above will trigger a failure exit code', choices: ['error', 'warn', 'info', 'hint'], - default: 'hint', // TODO: BREAKING: raise this to warn in 5.0 + default: 'error', type: 'string', }, 'display-only-failures': { @@ -76,6 +100,11 @@ const lintCommand: CommandModule = { type: 'boolean', default: false, }, + 'ignore-unknown-format': { + description: 'do not warn about unmatched formats', + type: 'boolean', + default: false, + }, verbose: { alias: 'v', description: 'increase verbosity', @@ -97,14 +126,22 @@ const lintCommand: CommandModule = { format, output, encoding, + ignoreUnknownFormat, ...config } = (args as unknown) as ILintConfig & { - documents: string[]; + documents: Array; failSeverity: FailSeverity; displayOnlyFailures: boolean; }; - return lint(documents, { format, output, encoding, ruleset, ...pick(config, ['skipRule', 'verbose', 'quiet']) }) + return lint(documents, { + format, + output, + encoding, + ignoreUnknownFormat, + ruleset, + ...pick, keyof ILintConfig>(config, ['skipRule', 'verbose', 'quiet', 'resolver']), + }) .then(results => { if (displayOnlyFailures) { return filterResultsBySeverity(results, failSeverity); @@ -124,8 +161,8 @@ const lintCommand: CommandModule = { }, }; -const fail = (err: Error) => { - console.error(err); +const fail = ({ message }: Error) => { + console.error(message); process.exitCode = 2; }; diff --git a/src/cli/services/__tests__/__fixtures__/foo-document.yaml b/src/cli/services/__tests__/__fixtures__/foo-document.yaml new file mode 100644 index 000000000..a86cb2f7d --- /dev/null +++ b/src/cli/services/__tests__/__fixtures__/foo-document.yaml @@ -0,0 +1 @@ +$ref: foo://openapi-3.0-no-contact.yaml diff --git a/src/cli/services/__tests__/__fixtures__/foo-resolver.js b/src/cli/services/__tests__/__fixtures__/foo-resolver.js new file mode 100644 index 000000000..31b1ad642 --- /dev/null +++ b/src/cli/services/__tests__/__fixtures__/foo-resolver.js @@ -0,0 +1,21 @@ +const fs = require('fs'); +const path = require('path'); +const { Resolver } = require('@stoplight/json-ref-resolver'); + +module.exports = new Resolver({ + resolvers: { + foo: { + resolve(ref) { + return new Promise((resolve, reject) => { + fs.readFile(path.join(__dirname, ref.hostname()), 'utf8', (err, data) => { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); + } + }, + }, +}); diff --git a/src/cli/services/__tests__/__fixtures__/openapi-3.0-valid.yaml b/src/cli/services/__tests__/__fixtures__/openapi-3.0-valid.yaml index d4f860dca..21f878339 100644 --- a/src/cli/services/__tests__/__fixtures__/openapi-3.0-valid.yaml +++ b/src/cli/services/__tests__/__fixtures__/openapi-3.0-valid.yaml @@ -9,4 +9,6 @@ info: servers: - description: thing url: http://localhost -paths: {} \ No newline at end of file +paths: {} +tags: + - name: my-tag diff --git a/src/cli/services/__tests__/linter.test.ts b/src/cli/services/__tests__/linter.test.ts index 58c909628..bc978e5bd 100644 --- a/src/cli/services/__tests__/linter.test.ts +++ b/src/cli/services/__tests__/linter.test.ts @@ -14,12 +14,13 @@ const invalidRulesetPath = resolve(__dirname, '__fixtures__/ruleset-invalid.yaml const validRulesetPath = resolve(__dirname, '__fixtures__/ruleset-valid.yaml'); const validNestedRulesetPath = resolve(__dirname, '__fixtures__/ruleset-extends-valid.yaml'); const invalidNestedRulesetPath = resolve(__dirname, '__fixtures__/ruleset-extends-invalid.yaml'); -const standardOas3RulesetPath = resolve(__dirname, '../../../rulesets/oas3/index.json'); -const standardOas2RulesetPath = resolve(__dirname, '../../../rulesets/oas2/index.json'); +const standardOasRulesetPath = resolve(__dirname, '../../../rulesets/oas/index.json'); const draftRefSpec = resolve(__dirname, './__fixtures__/draft-ref.oas2.json'); const draftNestedRefSpec = resolve(__dirname, './__fixtures__/draft-nested-ref.oas2.json'); const validOas3SpecPath = resolve(__dirname, './__fixtures__/openapi-3.0-valid.yaml'); const invalidOas3SpecPath = resolve(__dirname, '__fixtures__/openapi-3.0-no-contact.yaml'); +const fooResolver = resolve(__dirname, '__fixtures__/foo-resolver.js'); +const fooDocument = resolve(__dirname, '__fixtures__/foo-document.yaml'); function run(command: string) { const parser = yargs.command(lintCommand); @@ -54,6 +55,7 @@ describe('Linter service', () => { expect.arrayContaining([ expect.objectContaining({ code: 'oas3-schema', + path: ['info', 'contact', 'name'], range: { end: { character: 14, @@ -118,7 +120,7 @@ describe('Linter service', () => { expect(logSpy).toHaveBeenCalledWith('OpenAPI 3.x detected'); expect(output).toEqual( expect.arrayContaining([ - expect.objectContaining({ code: 'api-servers' }), + expect.objectContaining({ code: 'oas3-api-servers' }), expect.objectContaining({ code: 'info-contact' }), ]), ); @@ -128,17 +130,17 @@ describe('Linter service', () => { it('output other warnings but not info-contact', async () => { const output = await run(`lint --skip-rule=info-contact ${document}`); - expect(output).toEqual(expect.arrayContaining([expect.objectContaining({ code: 'api-servers' })])); + expect(output).toEqual(expect.arrayContaining([expect.objectContaining({ code: 'oas3-api-servers' })])); expect(output).toEqual(expect.not.arrayContaining([expect.objectContaining({ code: 'info-contact' })])); }); }); - describe('and --skip-rule=info-contact --skip-rule=api-servers is set', () => { - it('outputs neither info-contact or api-servers', async () => { - const output = await run(`lint --skip-rule=info-contact --skip-rule=api-servers ${document}`); + describe('and --skip-rule=info-contact --skip-rule=oas3-api-servers is set', () => { + it('outputs neither info-contact or oas3-api-servers', async () => { + const output = await run(`lint --skip-rule=info-contact --skip-rule=oas3-api-servers ${document}`); expect(output).toEqual(expect.not.arrayContaining([expect.objectContaining({ code: 'info-contact' })])); - expect(output).toEqual(expect.not.arrayContaining([expect.objectContaining({ code: 'api-servers' })])); + expect(output).toEqual(expect.not.arrayContaining([expect.objectContaining({ code: 'oas3-api-servers' })])); }); }); }); @@ -165,7 +167,7 @@ describe('Linter service', () => { source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'), }), expect.objectContaining({ - code: 'valid-example-in-schemas', + code: 'oas3-valid-schema-example', path: ['components', 'schemas', 'foo', 'example'], source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'), }), @@ -217,7 +219,7 @@ describe('Linter service', () => { source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'), }), expect.objectContaining({ - code: 'valid-example-in-schemas', + code: 'oas3-valid-schema-example', path: ['components', 'schemas', 'foo', 'example'], source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'), }), @@ -364,6 +366,43 @@ describe('Linter service', () => { ]), ); }); + + it('unixifies patterns', () => { + return expect(run(`lint src\\__tests__\\__fixtures__\\petstore.invalid-schema.*.json`)).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: 'invalid-ref', + path: ['paths', '/pets', 'get', 'responses', '200', 'content', 'application/json', 'schema', '$ref'], + source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'), + }), + expect.objectContaining({ + code: 'invalid-ref', + path: ['paths', '/pets', 'get', 'responses', 'default', 'content', 'application/json', 'schema', '$ref'], + source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'), + }), + expect.objectContaining({ + code: 'invalid-ref', + path: ['paths', '/pets', 'get', 'responses', 'default', 'content', 'application/json', 'schema', '$ref'], + source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'), + }), + expect.objectContaining({ + code: 'oas3-valid-schema-example', + path: ['components', 'schemas', 'foo', 'example'], + source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'), + }), + expect.objectContaining({ + code: 'oas3-unused-components-schema', + path: ['components', 'schemas', 'Pets'], + source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'), + }), + expect.objectContaining({ + code: 'oas3-schema', + path: ['paths', '/pets', 'get', 'responses', '200'], + source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'), + }), + ]), + ); + }); }); describe('--ruleset', () => { @@ -442,9 +481,9 @@ describe('Linter service', () => { describe('when a standard oas3 ruleset provided through option', () => { it('outputs warnings', () => { - return expect(run(`lint ${invalidOas3SpecPath} -r ${standardOas3RulesetPath}`)).resolves.toEqual( + return expect(run(`lint ${invalidOas3SpecPath} -r ${standardOasRulesetPath}`)).resolves.toEqual( expect.arrayContaining([ - expect.objectContaining({ code: 'api-servers' }), + expect.objectContaining({ code: 'oas3-api-servers' }), expect.objectContaining({ code: 'info-contact' }), expect.objectContaining({ code: 'info-description' }), ]), @@ -454,7 +493,7 @@ describe('Linter service', () => { describe('when a standard oas2 ruleset provided through option', () => { it('outputs warnings', async () => { - const output = await run(`lint ${oas2PetstoreSpecPath} -r ${standardOas2RulesetPath}`); + const output = await run(`lint ${oas2PetstoreSpecPath} -r ${standardOasRulesetPath}`); expect(output).toEqual(expect.arrayContaining([expect.objectContaining({ code: 'operation-description' })])); expect(output).toHaveLength(22); }); @@ -520,16 +559,23 @@ describe('Linter service', () => { it('outputs errors occurring in referenced files', () => { return expect(run(`lint ${draftRefSpec}`)).resolves.toEqual([ expect.objectContaining({ - code: 'api-schemes', + code: 'oas2-api-schemes', message: 'OpenAPI host `schemes` must be present and non-empty array.', path: [], range: expect.any(Object), source: expect.stringContaining('__tests__/__fixtures__/draft-ref.oas2.json'), }), + expect.objectContaining({ + code: 'openapi-tags', + message: 'OpenAPI object should have non-empty `tags` array.', + path: [], + range: expect.any(Object), + source: expect.stringContaining('__tests__/__fixtures__/draft-ref.oas2.json'), + }), expect.objectContaining({ code: 'oas2-schema', - message: '/info Property foo is not expected to be here', - path: ['info'], + message: 'property foo is not expected to be here', + path: ['definitions', 'info'], range: { end: { character: 5, @@ -540,12 +586,12 @@ describe('Linter service', () => { line: 3, }, }, - source: expect.stringContaining('__tests__/__fixtures__/refs/info.json'), + source: expect.stringContaining('/__tests__/__fixtures__/refs/info.json'), }), expect.objectContaining({ code: 'info-description', message: 'OpenAPI object info `description` must be present and non-empty string.', - path: ['info', 'description'], // todo: relative path or absolute path? there is no such path in linted file, but there is such in spec when working on resolved file + path: ['definitions', 'info', 'description'], range: { end: { character: 22, @@ -564,16 +610,23 @@ describe('Linter service', () => { it('outputs errors occurring in nested referenced files', () => { return expect(run(`lint ${draftNestedRefSpec}`)).resolves.toEqual([ expect.objectContaining({ - code: 'api-schemes', + code: 'oas2-api-schemes', message: 'OpenAPI host `schemes` must be present and non-empty array.', path: [], range: expect.any(Object), source: expect.stringContaining('__tests__/__fixtures__/draft-nested-ref.oas2.json'), }), + expect.objectContaining({ + code: 'openapi-tags', + message: 'OpenAPI object should have non-empty `tags` array.', + path: [], + range: expect.any(Object), + source: expect.stringContaining('__tests__/__fixtures__/draft-nested-ref.oas2.json'), + }), expect.objectContaining({ code: 'oas2-schema', - message: "/info should have required property 'title'", - path: ['info'], + message: `object should have required property \`title\``, + path: [], range: { end: { character: 1, @@ -589,7 +642,7 @@ describe('Linter service', () => { expect.objectContaining({ code: 'info-description', message: 'OpenAPI object info `description` must be present and non-empty string.', - path: ['info', 'description'], + path: ['description'], range: { end: { character: 18, @@ -653,4 +706,29 @@ describe('Linter service', () => { ]); }); }); + + describe('--resolver', () => { + it('uses provided resolver for $ref resolving', async () => { + expect(await run(`lint --resolver ${fooResolver} ${fooDocument}`)).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: 'info-contact', + source: 'foo://openapi-3.0-no-contact.yaml', + }), + expect.objectContaining({ + code: 'info-description', + source: 'foo://openapi-3.0-no-contact.yaml', + }), + expect.objectContaining({ + code: 'oas3-api-servers', + source: 'foo://openapi-3.0-no-contact.yaml', + }), + expect.objectContaining({ + code: 'openapi-tags', + source: 'foo://openapi-3.0-no-contact.yaml', + }), + ]), + ); + }); + }); }); diff --git a/src/cli/services/__tests__/output.test.ts b/src/cli/services/__tests__/output.test.ts new file mode 100644 index 000000000..66854f99e --- /dev/null +++ b/src/cli/services/__tests__/output.test.ts @@ -0,0 +1,67 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import * as fs from 'fs'; +import * as formatters from '../../../formatters'; +import { OutputFormat } from '../../../types/config'; +import { formatOutput, writeOutput } from '../output'; + +jest.mock('../../../formatters'); +jest.mock('fs'); + +describe('Output service', () => { + describe('formatOutput', () => { + it.each(['stylish', 'json', 'junit'])('calls %s formatter with given result', format => { + const results = [ + { + code: 'info-contact', + path: ['info'], + message: 'Info object should contain `contact` object.', + severity: DiagnosticSeverity.Information, + range: { + start: { + line: 2, + character: 9, + }, + end: { + line: 6, + character: 19, + }, + }, + source: '/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json', + }, + ]; + + const output = `value for ${format}`; + (formatters[format] as jest.Mock).mockReturnValueOnce(output); + expect(formatOutput(results, format as OutputFormat)).toEqual(output); + }); + }); + + describe('writeOutput', () => { + let logSpy: jest.SpyInstance; + + beforeEach(() => { + logSpy = jest.spyOn(console, 'log'); + }); + + afterEach(() => { + logSpy.mockRestore(); + }); + + it('given outputFile, writes output to a specified path', async () => { + ((fs.writeFile as any) as jest.Mock).mockImplementationOnce((path, val, cb) => { + cb(null, void 0); + }); + + const output = '{}'; + const outputFile = 'foo.json'; + expect(await writeOutput(output, outputFile)).toBeUndefined(); + expect(fs.writeFile).toBeCalledWith(outputFile, output, expect.any(Function)); + }); + + it('given no outputFile, print output to console', async () => { + const output = '{}'; + expect(await writeOutput(output)).toBeUndefined(); + expect(logSpy).toBeCalledWith(output); + }); + }); +}); diff --git a/src/cli/services/linter/linter.ts b/src/cli/services/linter/linter.ts index 0520072bd..c00236a07 100644 --- a/src/cli/services/linter/linter.ts +++ b/src/cli/services/linter/linter.ts @@ -1,5 +1,5 @@ import { IParserResult } from '@stoplight/types'; -import { getLocationForJsonPath, parseWithPointers } from '@stoplight/yaml'; +import { getLocationForJsonPath } from '@stoplight/yaml'; import { isJSONSchema, @@ -12,12 +12,13 @@ import { isOpenApiv3, } from '../../../formats'; import { readParsable } from '../../../fs/reader'; -import { httpAndFileResolver } from '../../../resolvers/http-and-file'; +import { parseYaml } from '../../../parsers'; import { isRuleEnabled } from '../../../runner'; import { IRuleResult, Spectral } from '../../../spectral'; import { FormatLookup, IParsedResult } from '../../../types'; import { ILintConfig } from '../../../types/config'; import { getRuleset, listFiles, skipRules } from './utils'; +import { getResolver } from './utils/getResolver'; const KNOWN_FORMATS: Array<[string, FormatLookup, string]> = [ ['oas2', isOpenApiv2, 'OpenAPI 2.0 (Swagger) detected'], @@ -30,8 +31,10 @@ const KNOWN_FORMATS: Array<[string, FormatLookup, string]> = [ ['json-schema-2019-09', isJSONSchemaDraft2019_09, 'JSON Schema Draft 2019-09 detected'], ]; -export async function lint(documents: string[], flags: ILintConfig) { - const spectral = new Spectral({ resolver: httpAndFileResolver }); +export async function lint(documents: Array, flags: ILintConfig) { + const spectral = new Spectral({ + resolver: getResolver(flags.resolver), + }); const ruleset = await getRuleset(flags.ruleset); spectral.setRuleset(ruleset); @@ -64,28 +67,26 @@ export async function lint(documents: string[], flags: ILintConfig) { } const targetUris = await listFiles(documents); - const results: IRuleResult[] = []; // todo: shall we display results as they come in? + const results: IRuleResult[] = []; for (const targetUri of targetUris) { if (flags.verbose) { console.info(`Linting ${targetUri}`); } - const spec: IParserResult = parseWithPointers(await readParsable(targetUri, { encoding: flags.encoding }), { - ignoreDuplicateKeys: false, - mergeKeys: true, - }); + const spec: IParserResult = parseYaml(await readParsable(targetUri, { encoding: flags.encoding })); const parsedResult: IParsedResult = { - source: targetUri, + source: typeof targetUri === 'number' ? '' : targetUri, parsed: spec, getLocationForJsonPath, }; results.push( ...(await spectral.run(parsedResult, { + ignoreUnknownFormat: flags.ignoreUnknownFormat, resolve: { - documentUri: targetUri, + documentUri: typeof targetUri === 'number' ? void 0 : targetUri, }, })), ); diff --git a/src/cli/services/linter/utils/__tests__/__fixtures__/resolver.js b/src/cli/services/linter/utils/__tests__/__fixtures__/resolver.js new file mode 100644 index 000000000..5a799cde3 --- /dev/null +++ b/src/cli/services/linter/utils/__tests__/__fixtures__/resolver.js @@ -0,0 +1,5 @@ +const { Resolver } = require('@stoplight/json-ref-resolver'); + +module.exports = new Resolver({ + resolvers: {}, +}); diff --git a/src/cli/services/linter/utils/__tests__/getResolver.spec.ts b/src/cli/services/linter/utils/__tests__/getResolver.spec.ts new file mode 100644 index 000000000..22d65fe59 --- /dev/null +++ b/src/cli/services/linter/utils/__tests__/getResolver.spec.ts @@ -0,0 +1,26 @@ +import { join, relative } from '@stoplight/path'; +import { httpAndFileResolver } from '../../../../../resolvers/http-and-file'; +import { getResolver } from '../getResolver'; + +const customResolver = require('./__fixtures__/resolver'); + +describe('getResolver', () => { + it('resolves absolute path to the file', () => { + expect(getResolver(join(__dirname, './__fixtures__/resolver.js'))).toStrictEqual(customResolver); + }); + + it('resolves relative path to the file', () => { + const relativePath = relative(process.cwd(), join(__dirname, './__fixtures__/resolver.js')); + expect(getResolver(relativePath)).toStrictEqual(customResolver); + }); + + it('throws when module cannot be imported', () => { + expect(getResolver.bind(null, join(__dirname, 'test.json'))).toThrow( + `Cannot find module '${join(__dirname, 'test.json')}' from 'getResolver.ts'`, + ); + }); + + it('given no path, returns default resolver', () => { + expect(getResolver(void 0)).toBe(httpAndFileResolver); + }); +}); diff --git a/src/cli/services/linter/utils/__tests__/listFiles.spec.ts b/src/cli/services/linter/utils/__tests__/listFiles.spec.ts new file mode 100644 index 000000000..4c6e5dc30 --- /dev/null +++ b/src/cli/services/linter/utils/__tests__/listFiles.spec.ts @@ -0,0 +1,23 @@ +import * as path from '@stoplight/path'; +import * as fg from 'fast-glob'; +import { listFiles } from '../listFiles'; + +jest.mock('fast-glob', () => jest.fn(async () => [])); + +describe('listFiles CLI util', () => { + it('unixify paths', () => { + listFiles(['.\\repro\\lib.yaml', './foo/*.json', '.\\src\\__tests__\\__fixtures__\\*.oas.json']); + expect(fg).toBeCalledWith(['./repro/lib.yaml', './foo/*.json', './src/__tests__/__fixtures__/*.oas.json'], { + dot: true, + absolute: true, + }); + }); + + it('returns file paths', async () => { + const list = [path.join(__dirname, 'foo/a.json'), path.join(__dirname, 'foo/b.json')]; + + ((fg as unknown) as jest.Mock).mockResolvedValueOnce([...list]); + + expect(await listFiles(['./foo/*.json'])).toEqual(list); + }); +}); diff --git a/src/cli/services/linter/utils/getResolver.ts b/src/cli/services/linter/utils/getResolver.ts new file mode 100644 index 000000000..fd3322a3f --- /dev/null +++ b/src/cli/services/linter/utils/getResolver.ts @@ -0,0 +1,19 @@ +import { isAbsolute, join } from '@stoplight/path'; +import { Optional } from '@stoplight/types'; +import { httpAndFileResolver } from '../../../../resolvers/http-and-file'; + +export const getResolver = (resolver: Optional) => { + if (resolver) { + try { + return require(isAbsolute(resolver) ? resolver : join(process.cwd(), resolver)); + } catch ({ message }) { + throw new Error(formatMessage(message) ?? message); + } + } + + return httpAndFileResolver; +}; + +function formatMessage(message: string): Optional { + return message.split(/\r?\n/)?.[0]?.replace(/\\/g, '/'); +} diff --git a/src/cli/services/linter/utils/listFiles.ts b/src/cli/services/linter/utils/listFiles.ts index 0f57cd7c3..b490db175 100644 --- a/src/cli/services/linter/utils/listFiles.ts +++ b/src/cli/services/linter/utils/listFiles.ts @@ -1,11 +1,17 @@ import { normalize } from '@stoplight/path'; import * as fg from 'fast-glob'; -export async function listFiles(pattens: string[]): Promise { - const { files, urls } = pattens.reduce<{ files: string[]; urls: string[] }>( +export async function listFiles(pattens: Array): Promise> { + const { files, fileDescriptors, urls } = pattens.reduce<{ + files: string[]; + urls: string[]; + fileDescriptors: number[]; + }>( (group, pattern) => { - if (!/^https?:\/\//.test(pattern)) { - group.files.push(pattern); + if (typeof pattern === 'number') { + group.fileDescriptors.push(pattern); + } else if (!/^https?:\/\//.test(pattern)) { + group.files.push(pattern.replace(/\\/g, '/')); } else { group.urls.push(pattern); } @@ -15,8 +21,9 @@ export async function listFiles(pattens: string[]): Promise { { files: [], urls: [], + fileDescriptors: [], }, ); - return [...urls, ...(await fg(files, { dot: true, absolute: true })).map(normalize)]; // let's normalize OS paths produced by fast-glob to have consistent paths across all platforms + return [...urls, ...fileDescriptors, ...(await fg(files, { dot: true, absolute: true })).map(normalize)]; // let's normalize OS paths produced by fast-glob to have consistent paths across all platforms } diff --git a/src/cli/services/output.ts b/src/cli/services/output.ts index cdbe02afd..6ec81f4ba 100644 --- a/src/cli/services/output.ts +++ b/src/cli/services/output.ts @@ -1,7 +1,7 @@ import { Dictionary } from '@stoplight/types'; import { writeFile } from 'fs'; import { promisify } from 'util'; -import { json, junit, stylish } from '../../formatters'; +import { html, json, junit, stylish, teamcity, text } from '../../formatters'; import { Formatter } from '../../formatters/types'; import { IRuleResult } from '../../types'; import { OutputFormat } from '../../types/config'; @@ -12,6 +12,9 @@ const formatters: Dictionary = { json, stylish, junit, + html, + text, + teamcity, }; export function formatOutput(results: IRuleResult[], format: OutputFormat): string { diff --git a/src/error-messages.ts b/src/error-messages.ts index 010087e74..517d404b8 100644 --- a/src/error-messages.ts +++ b/src/error-messages.ts @@ -53,7 +53,7 @@ export const formatResolverErrors = (resolved: Resolved): IRuleResult[] => { message: prettyPrintResolverErrorMessage(error.message), severity: DiagnosticSeverity.Error, range: location.range, - source: resolved.spec.source, + source: error.uriStack.length > 0 ? error.uriStack[error.uriStack.length - 1] : resolved.source, }); } diff --git a/src/formats/json-schema.ts b/src/formats/json-schema.ts index 5d75a25f5..cdb93b5b7 100644 --- a/src/formats/json-schema.ts +++ b/src/formats/json-schema.ts @@ -1,8 +1,6 @@ -import { JSONSchema4, JSONSchema6, JSONSchema7 } from 'json-schema'; +import { JSONSchema } from '../types'; import { isObject } from '../utils/isObject'; -type JSONSchema = JSONSchema4 | JSONSchema6 | JSONSchema7; - const KNOWN_JSON_SCHEMA_TYPES = ['array', 'boolean', 'integer', 'null', 'number', 'object', 'string']; const KNOWN_JSON_SCHEMA_COMBINERS = ['allOf', 'oneOf', 'anyOf']; const $SCHEMA_DRAFT4_REGEX = /^https?:\/\/json-schema.org\/draft-04\/(?:hyper-)?schema#?$/; diff --git a/src/formatters/__tests__/html.test.ts b/src/formatters/__tests__/html.test.ts new file mode 100644 index 000000000..ec67396a7 --- /dev/null +++ b/src/formatters/__tests__/html.test.ts @@ -0,0 +1,53 @@ +import { HTMLElement, parse } from 'node-html-parser'; +import { sortResults } from '../../utils'; +import { html } from '../html'; + +const mixedErrors = sortResults(require('./__fixtures__/mixed-errors.json')); + +describe('HTML formatter', () => { + test('should display proper severity levels', () => { + const result = parse(html(mixedErrors)) as HTMLElement; + const table = result.querySelector('table tbody'); + expect(table.innerHTML.trim()).toEqual(` + + [+] /home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json + 6 problems (1 error, 1 warning, 3 infos, 1 hint) + + + + 3:10 + hint + Info object should contain \`contact\` object. + + + + 3:10 + warning + OpenAPI object info \`description\` must be present and non-empty string. + + + + 5:14 + error + Info must contain Stoplight + + + + 17:13 + information + Operation \`description\` must be present and non-empty string. + + + + 64:14 + information + Operation \`description\` must be present and non-empty string. + + + + 86:13 + information + Operation \`description\` must be present and non-empty string. +`); + }); +}); diff --git a/src/formatters/__tests__/json.test.ts b/src/formatters/__tests__/json.test.ts index d75f8e08a..8aaab4510 100644 --- a/src/formatters/__tests__/json.test.ts +++ b/src/formatters/__tests__/json.test.ts @@ -1,7 +1,8 @@ import { IRuleResult } from '../../types'; +import { sortResults } from '../../utils'; import { json } from '../json'; -const results: IRuleResult[] = [ +const results: IRuleResult[] = sortResults([ { code: 'operation-description', message: 'paths./pets.get.description is not truthy', @@ -36,7 +37,7 @@ const results: IRuleResult[] = [ }, }, }, -]; +]); describe('JSON formatter', () => { test('should include ranges', () => { diff --git a/src/formatters/__tests__/junit.test.ts b/src/formatters/__tests__/junit.test.ts index 1019b62a0..b224898dc 100644 --- a/src/formatters/__tests__/junit.test.ts +++ b/src/formatters/__tests__/junit.test.ts @@ -1,9 +1,10 @@ import { promisify } from 'util'; import { Parser } from 'xml2js'; +import { sortResults } from '../../utils'; import { junit } from '../junit'; -const oas3SchemaErrors = require('./__fixtures__/oas3-schema-errors.json'); -const mixedErrors = require('./__fixtures__/mixed-errors-with-different-paths.json'); +const oas3SchemaErrors = sortResults(require('./__fixtures__/oas3-schema-errors.json')); +const mixedErrors = sortResults(require('./__fixtures__/mixed-errors.json')); describe('JUnit formatter', () => { let parse: Parser['parseString']; diff --git a/src/formatters/__tests__/stylish.test.ts b/src/formatters/__tests__/stylish.test.ts index fcf507d1d..b262c3a94 100644 --- a/src/formatters/__tests__/stylish.test.ts +++ b/src/formatters/__tests__/stylish.test.ts @@ -1,8 +1,9 @@ -import chalk from 'chalk'; +import * as chalk from 'chalk'; +import { sortResults } from '../../utils'; import { stylish } from '../stylish'; -const oas3SchemaErrors = require('./__fixtures__/oas3-schema-errors.json'); -const mixedErrors = require('./__fixtures__/mixed-errors.json'); +const oas3SchemaErrors = sortResults(require('./__fixtures__/oas3-schema-errors.json')); +const mixedErrors = sortResults(require('./__fixtures__/mixed-errors.json')); describe('Stylish formatter', () => { test('should prefer message for oas-schema errors', () => { diff --git a/src/formatters/__tests__/teamcity.test.ts b/src/formatters/__tests__/teamcity.test.ts new file mode 100644 index 000000000..d3b4f7026 --- /dev/null +++ b/src/formatters/__tests__/teamcity.test.ts @@ -0,0 +1,23 @@ +import { sortResults } from '../../utils'; +import { teamcity } from '../teamcity'; + +const mixedErrors = sortResults(require('./__fixtures__/mixed-errors.json')); + +describe('Teamcity formatter', () => { + test('should format messages', () => { + const result = teamcity(mixedErrors); + expect(result) + .toContain(`##teamcity[inspectionType category='openapi' id='info-contact' name='info-contact' description='hint -- Info object should contain \`contact\` object.'] +##teamcity[inspection typeId='info-contact' file='/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json' line='3' message='hint -- Info object should contain \`contact\` object.'] +##teamcity[inspectionType category='openapi' id='info-description' name='info-description' description='warning -- OpenAPI object info \`description\` must be present and non-empty string.'] +##teamcity[inspection typeId='info-description' file='/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json' line='3' message='warning -- OpenAPI object info \`description\` must be present and non-empty string.'] +##teamcity[inspectionType category='openapi' id='info-matches-stoplight' name='info-matches-stoplight' description='error -- Info must contain Stoplight'] +##teamcity[inspection typeId='info-matches-stoplight' file='/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json' line='5' message='error -- Info must contain Stoplight'] +##teamcity[inspectionType category='openapi' id='operation-description' name='operation-description' description='information -- Operation \`description\` must be present and non-empty string.'] +##teamcity[inspection typeId='operation-description' file='/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json' line='17' message='information -- Operation \`description\` must be present and non-empty string.'] +##teamcity[inspectionType category='openapi' id='operation-description' name='operation-description' description='information -- Operation \`description\` must be present and non-empty string.'] +##teamcity[inspection typeId='operation-description' file='/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json' line='64' message='information -- Operation \`description\` must be present and non-empty string.'] +##teamcity[inspectionType category='openapi' id='operation-description' name='operation-description' description='information -- Operation \`description\` must be present and non-empty string.'] +##teamcity[inspection typeId='operation-description' file='/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json' line='86' message='information -- Operation \`description\` must be present and non-empty string.']`); + }); +}); diff --git a/src/formatters/__tests__/text.test.ts b/src/formatters/__tests__/text.test.ts new file mode 100644 index 000000000..7afcd452a --- /dev/null +++ b/src/formatters/__tests__/text.test.ts @@ -0,0 +1,17 @@ +import { sortResults } from '../../utils'; +import { text } from '../text'; + +const mixedErrors = sortResults(require('./__fixtures__/mixed-errors.json')); + +describe('Text formatter', () => { + test('should format messages', () => { + const result = text(mixedErrors); + expect(result) + .toContain(`/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json:3:10 hint info-contact "Info object should contain \`contact\` object." +/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json:3:10 warning info-description "OpenAPI object info \`description\` must be present and non-empty string." +/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json:5:14 error info-matches-stoplight "Info must contain Stoplight" +/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json:17:13 information operation-description "Operation \`description\` must be present and non-empty string." +/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json:64:14 information operation-description "Operation \`description\` must be present and non-empty string." +/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json:86:13 information operation-description "Operation \`description\` must be present and non-empty string."`); + }); +}); diff --git a/src/formatters/html/html-template-message.html b/src/formatters/html/html-template-message.html new file mode 100644 index 000000000..73f5ba9d1 --- /dev/null +++ b/src/formatters/html/html-template-message.html @@ -0,0 +1,5 @@ + + <%= line %>:<%= character %> + <%= severity %> + <%- message %> + diff --git a/src/formatters/html/html-template-page.html b/src/formatters/html/html-template-page.html new file mode 100644 index 000000000..a4f1c2bf1 --- /dev/null +++ b/src/formatters/html/html-template-page.html @@ -0,0 +1,118 @@ + + + + + + Spectral Report + + +
+

Spectral Report

+
+ <%= summary %> - Generated on <%= date %> +
+
+ + + <%= results %> + +
+ + + diff --git a/src/formatters/html/html-template-result.html b/src/formatters/html/html-template-result.html new file mode 100644 index 000000000..f4a55933c --- /dev/null +++ b/src/formatters/html/html-template-result.html @@ -0,0 +1,6 @@ + + + [+] <%- filePath %> + <%- summary %> + + diff --git a/src/formatters/html/index.ts b/src/formatters/html/index.ts new file mode 100644 index 000000000..505df7e64 --- /dev/null +++ b/src/formatters/html/index.ts @@ -0,0 +1,91 @@ +/** + * Copyright JS Foundation and other contributors, https://js.foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @fileoverview HTML reporter + * @author Julian Laval + */ +import * as path from '@stoplight/path'; +import { Dictionary } from '@stoplight/types'; +import * as eol from 'eol'; +import * as fs from 'fs'; +import { template } from 'lodash'; +import { IRuleResult } from '../../types'; +import { Formatter } from '../types'; +import { getHighestSeverity, getSeverityName, getSummary, getSummaryForSource, groupBySource } from '../utils'; + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +const pageTemplate = template(eol.lf(fs.readFileSync(path.join(__dirname, 'html-template-page.html'), 'utf8'))); +const messageTemplate = template(eol.lf(fs.readFileSync(path.join(__dirname, 'html-template-message.html'), 'utf8'))); +const resultTemplate = template(eol.lf(fs.readFileSync(path.join(__dirname, 'html-template-result.html'), 'utf8'))); + +function renderMessages(messages: IRuleResult[], parentIndex: number) { + return messages + .map(message => { + const line = message.range.start.line + 1; + const character = message.range.start.character + 1; + + return messageTemplate({ + parentIndex, + line, + character, + severity: getSeverityName(message.severity), + message: message.message, + code: message.code, + }); + }) + .join('\n'); +} + +function renderResults(groupedResults: Dictionary) { + return Object.keys(groupedResults) + .map( + (source, index) => + resultTemplate({ + index, + color: + groupedResults[source].length === 0 + ? 'success' + : getSeverityName(getHighestSeverity(groupedResults[source])), + filePath: source, + summary: getSummaryForSource(groupedResults[source]), + }) + renderMessages(groupedResults[source], index), + ) + .join('\n'); +} + +// ------------------------------------------------------------------------------ +// Public Interface +// ------------------------------------------------------------------------------ + +export const html: Formatter = results => { + const color = results.length === 0 ? 'success' : getSeverityName(getHighestSeverity(results)); + const groupedResults = groupBySource(results); + + return pageTemplate({ + date: new Date(), + color, + summary: getSummary(groupedResults), + results: renderResults(groupedResults), + }); +}; diff --git a/src/formatters/index.ts b/src/formatters/index.ts index c51aab7a7..7908adaa2 100644 --- a/src/formatters/index.ts +++ b/src/formatters/index.ts @@ -1,3 +1,6 @@ export * from './json'; export * from './stylish'; export * from './junit'; +export * from './html'; +export * from './text'; +export * from './teamcity'; diff --git a/src/formatters/junit.ts b/src/formatters/junit.ts index 543da66ea..91aae1ee8 100644 --- a/src/formatters/junit.ts +++ b/src/formatters/junit.ts @@ -23,17 +23,12 @@ * @author Jamund Ferguson */ -import { encodePointerFragment } from '@stoplight/json'; import { extname } from '@stoplight/path'; -import { DiagnosticSeverity, JsonPath } from '@stoplight/types'; +import { DiagnosticSeverity } from '@stoplight/types'; import { escapeRegExp } from 'lodash'; +import { printPath, PrintStyle } from '../utils'; import { Formatter } from './types'; -import { groupBySource } from './utils/groupBySource'; -import { xmlEscape } from './utils/xmlEscape'; - -function stringifyPath(path: JsonPath) { - return ['#', ...path.map(encodePointerFragment)].join('/'); -} +import { groupBySource, xmlEscape } from './utils'; export const junit: Formatter = results => { let output = ''; @@ -51,9 +46,7 @@ export const junit: Formatter = results => { result => result.severity === DiagnosticSeverity.Error, ); - output += `\n`; + output += `\n`; for (const result of filteredValidationResults) { output += ``; @@ -61,7 +54,7 @@ export const junit: Formatter = results => { output += ''; output += ``; output += '\n'; diff --git a/src/formatters/stylish.ts b/src/formatters/stylish.ts index cc7b355f6..20a5d0ad9 100644 --- a/src/formatters/stylish.ts +++ b/src/formatters/stylish.ts @@ -23,62 +23,30 @@ * @author Sindre Sorhus */ -import chalk from 'chalk'; -import stripAnsi from 'strip-ansi'; +import { DiagnosticSeverity, IRange } from '@stoplight/types'; +import * as chalk from 'chalk'; +import stripAnsi = require('strip-ansi'); import * as table from 'text-table'; -import { DiagnosticSeverity, IRange } from '@stoplight/types'; import { IRuleResult } from '../types'; import { Formatter } from './types'; -import { getHighestSeverity } from './utils/getHighestSeverity'; -import { groupBySeverity } from './utils/groupBySeverity'; -import { groupBySource } from './utils/groupBySource'; -import { sortResults } from './utils/sortResults'; +import { getColorForSeverity, getHighestSeverity, getSeverityName, getSummary, groupBySource } from './utils'; // ----------------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------------- -/** - * Given a word and a count, append an s if count is not one. - * @param {string} word A word in its singular form. - * @param {number} count A number controlling whether word should be pluralized. - * @returns {string} The original word with an s on the end if count is not one. - */ -function pluralize(word: string, count: number): string { - return count === 1 ? word : `${word}s`; -} - function formatRange(range?: IRange): string { if (!range) return ''; return ` ${range.start.line + 1}:${range.start.character + 1}`; } -const SEVERITY_COLORS = { - [DiagnosticSeverity.Error]: 'red', - [DiagnosticSeverity.Warning]: 'yellow', - [DiagnosticSeverity.Information]: 'blue', - [DiagnosticSeverity.Hint]: 'white', -}; - -function getColorForSeverity(severity: DiagnosticSeverity) { - return SEVERITY_COLORS[severity]; -} - function getMessageType(severity: DiagnosticSeverity) { const color = getColorForSeverity(severity); + const name = getSeverityName(severity); - switch (severity) { - case DiagnosticSeverity.Error: - return chalk[color]('error'); - case DiagnosticSeverity.Warning: - return chalk[color]('warning'); - case DiagnosticSeverity.Information: - return chalk[color]('information'); - default: - return chalk[color]('hint'); - } + return chalk[color](name); } // ----------------------------------------------------------------------------- @@ -87,34 +55,19 @@ function getMessageType(severity: DiagnosticSeverity) { export const stylish: Formatter = results => { let output = '\n'; - let errorCount = 0; - let warningCount = 0; - let infoCount = 0; - let hintCount = 0; + const groupedResults = groupBySource(results); const summaryColor = getColorForSeverity(getHighestSeverity(results)); + const summaryText = getSummary(groupedResults); - const groupedResults = groupBySource(results); - Object.keys(groupedResults).map((path, index) => { + Object.keys(groupedResults).map(path => { const pathResults = groupedResults[path]; - const { - [DiagnosticSeverity.Error]: errors, - [DiagnosticSeverity.Warning]: warnings, - [DiagnosticSeverity.Information]: infos, - [DiagnosticSeverity.Hint]: hints, - } = groupBySeverity(pathResults); - - errorCount += errors.length; - warningCount += warnings.length; - infoCount += infos.length; - hintCount += hints.length; - output += `${chalk.underline(path)}\n`; - const pathTableData = sortResults(pathResults).map((result: IRuleResult) => [ + const pathTableData = pathResults.map((result: IRuleResult) => [ formatRange(result.range), getMessageType(result.severity), - result.code !== undefined ? result.code : '', + result.code ?? '', result.message, ]); @@ -131,30 +84,11 @@ export const stylish: Formatter = results => { .join('\n')}\n\n`; }); - const total = errorCount + warningCount + infoCount + hintCount; - - if (total > 0) { - output += chalk[summaryColor].bold( - [ - '\u2716 ', - total, - pluralize(' problem', total), - ' (', - errorCount, - pluralize(' error', errorCount), - ', ', - warningCount, - pluralize(' warning', warningCount), - ', ', - infoCount, - pluralize(' info', infoCount), - ', ', - hintCount, - pluralize(' hint', hintCount), - ')\n', - ].join(''), - ); + if (summaryText === null) { + return ''; } - return total > 0 ? output : ''; + output += chalk[summaryColor].bold(`\u2716 ${summaryText}\n`); + + return output; }; diff --git a/src/formatters/teamcity.ts b/src/formatters/teamcity.ts new file mode 100644 index 000000000..50b54e9b5 --- /dev/null +++ b/src/formatters/teamcity.ts @@ -0,0 +1,50 @@ +import { Dictionary, Optional } from '@stoplight/types'; +import { IRuleResult } from '../types'; +import { Formatter } from './types'; +import { getSeverityName, groupBySource } from './utils'; + +function escapeString(str: Optional) { + if (str === void 0) { + return ''; + } + return String(str) + .replace(/\|/g, '||') + .replace(/'/g, "|'") + .replace(/\n/g, '|n') + .replace(/\r/g, '|r') + .replace(/\u0085/g, '|x') // TeamCity 6 + .replace(/\u2028/g, '|l') // TeamCity 6 + .replace(/\u2029/g, '|p') // TeamCity 6 + .replace(/\[/g, '|[') + .replace(/\]/g, '|]'); +} + +function inspectionType(result: IRuleResult) { + const code = escapeString(result.code); + const severity = getSeverityName(result.severity); + const message = escapeString(result.message); + return `##teamcity[inspectionType category='openapi' id='${code}' name='${code}' description='${severity} -- ${message}']`; +} + +function inspection(result: IRuleResult) { + const code = escapeString(result.code); + const severity = getSeverityName(result.severity); + const message = escapeString(result.message); + const line = result.range.start.line + 1; + return `##teamcity[inspection typeId='${code}' file='${result.source}' line='${line}' message='${severity} -- ${message}']`; +} + +function renderResults(results: IRuleResult[], parentIndex: number) { + return results.map(result => `${inspectionType(result)}\n${inspection(result)}`).join('\n'); +} + +function renderGroupedResults(groupedResults: Dictionary) { + return Object.keys(groupedResults) + .map((source, index) => renderResults(groupedResults[source], index)) + .join('\n'); +} + +export const teamcity: Formatter = results => { + const groupedResults = groupBySource(results); + return renderGroupedResults(groupedResults); +}; diff --git a/src/formatters/text.ts b/src/formatters/text.ts new file mode 100644 index 000000000..f6be00ce5 --- /dev/null +++ b/src/formatters/text.ts @@ -0,0 +1,26 @@ +import { Dictionary } from '@stoplight/types'; +import { IRuleResult } from '../types'; +import { Formatter } from './types'; +import { getSeverityName, groupBySource } from './utils'; + +function renderResults(results: IRuleResult[], parentIndex: number) { + return results + .map(result => { + const line = result.range.start.line + 1; + const character = result.range.start.character + 1; + const severity = getSeverityName(result.severity); + return `${result.source}:${line}:${character} ${severity} ${result.code} "${result.message}"`; + }) + .join('\n'); +} + +function renderGroupedResults(groupedResults: Dictionary) { + return Object.keys(groupedResults) + .map((source, index) => renderResults(groupedResults[source], index)) + .join('\n'); +} + +export const text: Formatter = results => { + const groupedResults = groupBySource(results); + return renderGroupedResults(groupedResults); +}; diff --git a/src/formatters/utils/getColorForSeverity.ts b/src/formatters/utils/getColorForSeverity.ts new file mode 100644 index 000000000..4205ad853 --- /dev/null +++ b/src/formatters/utils/getColorForSeverity.ts @@ -0,0 +1,12 @@ +import { DiagnosticSeverity } from '@stoplight/types'; + +const SEVERITY_COLORS = { + [DiagnosticSeverity.Error]: 'red', + [DiagnosticSeverity.Warning]: 'yellow', + [DiagnosticSeverity.Information]: 'blue', + [DiagnosticSeverity.Hint]: 'white', +}; + +export function getColorForSeverity(severity: DiagnosticSeverity) { + return SEVERITY_COLORS[severity]; +} diff --git a/src/formatters/utils/getSeverityName.ts b/src/formatters/utils/getSeverityName.ts new file mode 100644 index 000000000..c0710cd16 --- /dev/null +++ b/src/formatters/utils/getSeverityName.ts @@ -0,0 +1,12 @@ +import { DiagnosticSeverity, Dictionary } from '@stoplight/types'; + +const SEVERITY_NAMES: Dictionary = { + [DiagnosticSeverity.Error]: 'error', + [DiagnosticSeverity.Warning]: 'warning', + [DiagnosticSeverity.Information]: 'information', + [DiagnosticSeverity.Hint]: 'hint', +}; + +export function getSeverityName(severity: DiagnosticSeverity) { + return SEVERITY_NAMES[severity]; +} diff --git a/src/formatters/utils/getSummary.ts b/src/formatters/utils/getSummary.ts new file mode 100644 index 000000000..d3525c624 --- /dev/null +++ b/src/formatters/utils/getSummary.ts @@ -0,0 +1,78 @@ +import { DiagnosticSeverity, Dictionary } from '@stoplight/types'; +import { IRuleResult } from '../../types'; +import { groupBySeverity } from './groupBySeverity'; +import { pluralize } from './pluralize'; + +const printSummary = ({ + errors, + warnings, + infos, + hints, +}: { + errors: number; + warnings: number; + infos: number; + hints: number; +}) => { + const total = errors + warnings + infos + hints; + if (total === 0) { + return null; + } + + return [ + total, + pluralize(' problem', total), + ' (', + errors, + pluralize(' error', errors), + ', ', + warnings, + pluralize(' warning', warnings), + ', ', + infos, + pluralize(' info', infos), + ', ', + hints, + pluralize(' hint', hints), + ')', + ].join(''); +}; + +export const getSummaryForSource = (results: IRuleResult[]) => { + const { + [DiagnosticSeverity.Error]: { length: errors }, + [DiagnosticSeverity.Warning]: { length: warnings }, + [DiagnosticSeverity.Information]: { length: infos }, + [DiagnosticSeverity.Hint]: { length: hints }, + } = groupBySeverity(results); + + return printSummary({ + errors, + warnings, + infos, + hints, + }); +}; + +export const getSummary = (groupedResults: Dictionary): string | null => { + let errorCount = 0; + let warningCount = 0; + let infoCount = 0; + let hintCount = 0; + + for (const results of Object.values(groupedResults)) { + const { + [DiagnosticSeverity.Error]: errors, + [DiagnosticSeverity.Warning]: warnings, + [DiagnosticSeverity.Information]: infos, + [DiagnosticSeverity.Hint]: hints, + } = groupBySeverity(results); + + errorCount += errors.length; + warningCount += warnings.length; + infoCount += infos.length; + hintCount += hints.length; + } + + return printSummary({ errors: errorCount, warnings: warningCount, infos: infoCount, hints: hintCount }); +}; diff --git a/src/formatters/utils/index.ts b/src/formatters/utils/index.ts new file mode 100644 index 000000000..7733c6615 --- /dev/null +++ b/src/formatters/utils/index.ts @@ -0,0 +1,8 @@ +export * from './getColorForSeverity'; +export * from './getHighestSeverity'; +export * from './getSeverityName'; +export * from './getSummary'; +export * from './groupBySeverity'; +export * from './groupBySource'; +export * from './pluralize'; +export * from './xmlEscape'; diff --git a/src/formatters/utils/pluralize.ts b/src/formatters/utils/pluralize.ts new file mode 100644 index 000000000..31c4f042b --- /dev/null +++ b/src/formatters/utils/pluralize.ts @@ -0,0 +1,9 @@ +/** + * Given a word and a count, append an s if count is not one. + * @param {string} word A word in its singular form. + * @param {number} count A number controlling whether word should be pluralized. + * @returns {string} The original word with an s on the end if count is not one. + */ +export function pluralize(word: string, count: number): string { + return count === 1 ? word : `${word}s`; +} diff --git a/src/formatters/utils/sortResults.ts b/src/formatters/utils/sortResults.ts deleted file mode 100644 index b096f768f..000000000 --- a/src/formatters/utils/sortResults.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IRuleResult } from '../../types'; - -export const sortResults = (results: IRuleResult[]) => { - return [...results].sort((resultA, resultB) => { - const diff = resultA.range.start.line - resultB.range.start.line; - - if (diff === 0) { - return resultA.range.start.character - resultB.range.start.character; - } - - return diff; - }); -}; diff --git a/src/fs/__tests__/__fixtures__/simple b/src/fs/__tests__/__fixtures__/simple new file mode 100644 index 000000000..4443a906d --- /dev/null +++ b/src/fs/__tests__/__fixtures__/simple @@ -0,0 +1,3 @@ +line 1 +line 2 +end diff --git a/src/fs/__tests__/reader.jest.test.ts b/src/fs/__tests__/reader.jest.test.ts new file mode 100644 index 000000000..b49bf01a0 --- /dev/null +++ b/src/fs/__tests__/reader.jest.test.ts @@ -0,0 +1,36 @@ +import { join } from '@stoplight/path'; +import * as fs from 'fs'; +import { STATIC_ASSETS } from '../../assets'; +import { readFile } from '../reader'; + +describe('readFile util', () => { + describe('when a file descriptor is supplied', () => { + let fileDescriptor: number; + + beforeEach(() => { + fileDescriptor = fs.openSync(join(__dirname, '__fixtures__/simple'), 'r'); + }); + + afterEach(() => { + delete STATIC_ASSETS[fileDescriptor]; + }); + + it('reads from file', async () => { + const contents = await readFile(fileDescriptor, { encoding: 'utf8' }); + // normalize line endings + expect(contents.replace(/\r\n/g, '\n')).toEqual(`line 1 +line 2 +end +`); + }); + + it('always skips static assets', async () => { + STATIC_ASSETS[fileDescriptor] = 'test'; + expect(await readFile(fileDescriptor, { encoding: 'utf8' })).not.toEqual('test'); + }); + + it('throws when fd cannot be accessed', () => { + return expect(readFile(2, { encoding: 'utf8' })).rejects.toThrow(); + }); + }); +}); diff --git a/src/fs/reader.ts b/src/fs/reader.ts index dcc5a0a90..bafa818c8 100644 --- a/src/fs/reader.ts +++ b/src/fs/reader.ts @@ -1,6 +1,7 @@ import { isURL } from '@stoplight/path'; import AbortController from 'abort-controller'; import * as fs from 'fs'; +import { STATIC_ASSETS } from '../assets'; import request from '../request'; export interface IReadOptions { @@ -8,8 +9,31 @@ export interface IReadOptions { timeout?: number; } -export async function readFile(name: string, opts: IReadOptions): Promise { - if (isURL(name)) { +export async function readFile(name: string | number, opts: IReadOptions): Promise { + if (typeof name === 'number') { + let result = ''; + + const stream = fs.createReadStream('', { fd: name }); + stream.setEncoding(opts.encoding); + + stream.on('readable', () => { + let chunk: string | null; + + // tslint:disable-next-line:no-conditional-assignment + while ((chunk = stream.read()) !== null) { + result += chunk; + } + }); + + return new Promise((resolve, reject) => { + stream.on('error', reject); + stream.on('end', () => { + resolve(result); + }); + }); + } else if (name in STATIC_ASSETS) { + return STATIC_ASSETS[name]; + } else if (isURL(name)) { let response; let timeout: NodeJS.Timeout | number | null = null; try { @@ -53,7 +77,7 @@ export async function readFile(name: string, opts: IReadOptions): Promise { +export async function readParsable(name: string | number, opts: IReadOptions): Promise { try { return await readFile(name, opts); } catch (ex) { diff --git a/src/functions/__tests__/alphabetical.test.ts b/src/functions/__tests__/alphabetical.test.ts index bafe24937..78e439cf5 100644 --- a/src/functions/__tests__/alphabetical.test.ts +++ b/src/functions/__tests__/alphabetical.test.ts @@ -1,7 +1,8 @@ +import { parseYaml } from '../../parsers'; import { alphabetical } from '../alphabetical'; function runAlphabetical(target: any, keyedBy?: string) { - return alphabetical(target, { keyedBy }, { given: ['$'] }, { given: null, original: null }); + return alphabetical(target, { keyedBy }, { given: ['$'] }, { given: null, original: null } as any); } describe('alphabetical', () => { @@ -27,6 +28,21 @@ describe('alphabetical', () => { ]); }); + test('given an object with unsorted properties with numeric keys', () => { + const doc = parseYaml(` +'400': + description: '' +'200': + description: ''`).data; + + expect(runAlphabetical(doc)).toEqual([ + { + message: 'at least 2 properties are not in alphabetical order: "400" should be placed after "200"', + path: ['$', '400'], + }, + ]); + }); + describe('given NO keyedBy', () => { test('given an unsorted array of strings should return error', () => { expect(runAlphabetical(['b', 'a'])).toEqual([ diff --git a/src/functions/__tests__/casing.test.ts b/src/functions/__tests__/casing.test.ts new file mode 100644 index 000000000..4ee5f1110 --- /dev/null +++ b/src/functions/__tests__/casing.test.ts @@ -0,0 +1,221 @@ +import { ICasingOptions } from '../../types'; +import { casing } from '../casing'; + +function runCasing(target: unknown, type: ICasingOptions['type'], disallowDigits?: boolean) { + return casing( + target, + { type, disallowDigits }, + { given: ['$'] }, + { given: null, original: null, resolved: {} as any }, + ); +} + +describe('casing', () => { + test('given non-string target should return nothing', () => { + expect(runCasing(false, 'camel')).toBeUndefined(); + expect(runCasing(1, 'camel')).toBeUndefined(); + }); + + test('given empty string target should return nothing', () => { + expect(runCasing('', 'camel')).toBeUndefined(); + }); + + test('given unknown case type should return nothing', () => { + expect(runCasing('2', 'foo' as any)).toBeUndefined(); + }); + + describe('casing type', () => { + describe('flat', () => { + const invalid = ['foo_test', 'Foo', '123', '1d', 'foo-bar']; + const valid = ['foo', 'foobar']; + const validWithDigits = ['foo9bar', 'foo24baz', 'foo1']; + + test.each(invalid)('should recognize invalid target %s', target => { + expect(runCasing(target, 'flat')).toEqual([{ message: 'must be flat case' }]); + }); + + test.each([...valid, ...validWithDigits])('should recognize valid target %s', target => { + expect(runCasing(target, 'flat')).toBeUndefined(); + }); + + describe('when digits are disallowed', () => { + test.each([...invalid, ...validWithDigits])('should recognize invalid target %s', target => { + expect(runCasing(target, 'flat', true)).toEqual([{ message: 'must be flat case' }]); + }); + + test.each(valid)('should recognize valid target %s', target => { + expect(runCasing(target, 'flat', true)).toBeUndefined(); + }); + }); + }); + + describe('camel', () => { + const invalid = ['foo_test', 'Foo', '1fooBarBaz', '123', 'foo-bar']; + const valid = ['foo', 'fooBar', 'fooBarBaz']; + const validWithDigits = ['foo1', 'foo24Bar', 'fooBar0Baz323']; + + test.each(invalid)('should recognize invalid target %s', target => { + expect(runCasing(target, 'camel')).toEqual([{ message: 'must be camel case' }]); + }); + + test.each([valid, ...validWithDigits])('should recognize valid target %s', target => { + expect(runCasing(target, 'camel')).toBeUndefined(); + }); + + describe('when digits are disallowed', () => { + test.each([...invalid, ...validWithDigits])('should recognize invalid target %s', target => { + expect(runCasing(target, 'camel', true)).toEqual([{ message: 'must be camel case' }]); + }); + + test.each(valid)('should recognize valid target %s', target => { + expect(runCasing(target, 'camel', true)).toBeUndefined(); + }); + }); + }); + + describe('pascal', () => { + const invalid = ['foo_test', '123', '1fooBarBaz', 'fooBarBaz1', 'fooBar', 'foo1', 'foo-bar']; + const valid = ['Foo', 'FooBar', 'FooBarBaz']; + const validWithDigits = ['Foo1', 'FooBarBaz1']; + + test.each(invalid)('should recognize invalid target %s', target => { + expect(runCasing(target, 'pascal')).toEqual([{ message: 'must be pascal case' }]); + }); + + test.each([valid, ...validWithDigits])('should recognize valid target %s', target => { + expect(runCasing(target, 'pascal')).toBeUndefined(); + }); + + describe('when digits are disallowed', () => { + test.each([...invalid, ...validWithDigits])('should recognize invalid target %s', target => { + expect(runCasing(target, 'pascal', true)).toEqual([{ message: 'must be pascal case' }]); + }); + + test.each(valid)('should recognize valid target %s', target => { + expect(runCasing(target, 'pascal', true)).toBeUndefined(); + }); + }); + }); + + describe('kebab', () => { + const invalid = [ + 'foo_test', + 'Foo1', + '123', + 'fooBarBaz1', + 'fooBar', + 'foO', + 'foo-baR', + '1foo-bar', + 'foo--bar', + 'foo-', + '-foo', + ]; + + const valid = ['foo', 'foo-bar', 'foo-bar-baz']; + const validWithDigits = ['foo-bar1', 'foo1-2bar']; + + test.each(invalid)('should recognize invalid target %s', target => { + expect(runCasing(target, 'kebab')).toEqual([{ message: 'must be kebab case' }]); + }); + + test.each([...valid, ...validWithDigits])('should recognize valid target %s', target => { + expect(runCasing(target, 'kebab')).toBeUndefined(); + }); + + describe('when digits are disallowed', () => { + test.each([...invalid, ...validWithDigits])('should recognize invalid target %s', target => { + expect(runCasing(target, 'kebab', true)).toEqual([{ message: 'must be kebab case' }]); + }); + + test.each(valid)('should recognize valid target %s', target => { + expect(runCasing(target, 'kebab', true)).toBeUndefined(); + }); + }); + }); + + describe('cobol', () => { + const invalid = ['foo_test', 'Foo1', '123', 'fooBarBaz1', 'FOo', 'FOO-BAr', 'FOO--BAR', 'FOO-', '-FOO']; + const valid = ['FOO', 'FOO-BAR', 'FOO-BAR-BAZ']; + const validWithDigits = ['FOO-BAR1', 'FOO2-3BAR1']; + + test.each(invalid)('should recognize invalid target %s', target => { + expect(runCasing(target, 'cobol')).toEqual([{ message: 'must be cobol case' }]); + }); + + test.each([...valid, ...validWithDigits])('should recognize valid target %s', target => { + expect(runCasing(target, 'cobol')).toBeUndefined(); + }); + + describe('when digits are disallowed', () => { + test.each([...invalid, ...validWithDigits])('should recognize invalid target %s', target => { + expect(runCasing(target, 'cobol', true)).toEqual([{ message: 'must be cobol case' }]); + }); + + test.each(valid)('should recognize valid target %s', target => { + expect(runCasing(target, 'cobol', true)).toBeUndefined(); + }); + }); + }); + + describe('snake', () => { + const invalid = ['Foo1', '123', 'fooBarBaz1', 'FOo', 'FOO-BAR', 'foo__bar', '1foo_bar1', 'foo_', '_foo']; + const valid = ['foo', 'foo_bar', 'foo_bar_baz']; + const validWithDigits = ['foo_bar1', 'foo2_4bar1']; + + test.each(invalid)('should recognize invalid target %s', target => { + expect(runCasing(target, 'snake')).toEqual([{ message: 'must be snake case' }]); + }); + + test.each([...valid, ...validWithDigits])('should recognize valid target %s', target => { + expect(runCasing(target, 'snake')).toBeUndefined(); + }); + + describe('when digits are disallowed', () => { + test.each([...invalid, ...validWithDigits])('should recognize invalid target %s', target => { + expect(runCasing(target, 'snake', true)).toEqual([{ message: 'must be snake case' }]); + }); + + test.each(valid)('should recognize valid target %s', target => { + expect(runCasing(target, 'snake', true)).toBeUndefined(); + }); + }); + }); + + describe('macro', () => { + const invalid = [ + 'foo_test', + 'Foo1', + '123', + 'fooBarBaz1', + 'FOo', + 'FOO-BAR', + 'FO__BAR', + '1FOO_BAR1', + 'FOO___BAR1', + 'FOO_', + '_FOO', + ]; + const valid = ['FOO', 'FOO_BAR', 'FOO_BAR_BAZ']; + const validWithDigits = ['FOO_BAR1', 'FOO2_4BAR1', 'FOO2_4_2']; + + test.each(invalid)('should recognize invalid target %s', target => { + expect(runCasing(target, 'macro')).toEqual([{ message: 'must be macro case' }]); + }); + + test.each([...valid, ...validWithDigits])('should recognize valid target %s', target => { + expect(runCasing(target, 'macro')).toBeUndefined(); + }); + + describe('when digits are disallowed', () => { + test.each([...invalid, ...validWithDigits])('should recognize invalid target %s', target => { + expect(runCasing(target, 'macro', true)).toEqual([{ message: 'must be macro case' }]); + }); + + test.each(valid)('should recognize valid target %s', target => { + expect(runCasing(target, 'macro', true)).toBeUndefined(); + }); + }); + }); + }); +}); diff --git a/src/functions/__tests__/enum.test.ts b/src/functions/__tests__/enum.test.ts index 1d59fd235..da326c462 100644 --- a/src/functions/__tests__/enum.test.ts +++ b/src/functions/__tests__/enum.test.ts @@ -12,7 +12,7 @@ function runEnum(targetVal: any, values: any[]) { { given: [], original: '', - }, + } as any, ); } diff --git a/src/functions/__tests__/falsy.test.ts b/src/functions/__tests__/falsy.test.ts index 6872e0e3b..9dbbd00dc 100644 --- a/src/functions/__tests__/falsy.test.ts +++ b/src/functions/__tests__/falsy.test.ts @@ -11,7 +11,7 @@ function runFalsy(targetVal: any, targetPath?: any) { { given: null, original: null, - }, + } as any, ); } @@ -27,7 +27,7 @@ describe('falsy', () => { test('returns error message if target value is not falsy', () => { expect(runFalsy(true)).toEqual([ { - message: 'property is not falsy', + message: '{{property|gravis|append-property}}is not falsy', }, ]); }); diff --git a/src/functions/__tests__/pattern.test.ts b/src/functions/__tests__/pattern.test.ts index d700328df..a79bfa065 100644 --- a/src/functions/__tests__/pattern.test.ts +++ b/src/functions/__tests__/pattern.test.ts @@ -10,7 +10,7 @@ function runPattern(targetVal: any, options?: any) { { given: null, original: null, - }, + } as any, ); } diff --git a/src/functions/__tests__/schema-path.test.ts b/src/functions/__tests__/schema-path.test.ts index 9fa457f78..2e3cf5a39 100644 --- a/src/functions/__tests__/schema-path.test.ts +++ b/src/functions/__tests__/schema-path.test.ts @@ -1,12 +1,10 @@ import { schemaPath } from '../schema-path'; function runSchemaPath(target: any, field: string, schemaPathStr: string) { - return schemaPath( - target, - { field, schemaPath: schemaPathStr }, - { given: [], target: [] }, - { given: null, original: target }, - ); + return schemaPath(target, { field, schemaPath: schemaPathStr }, { given: [], target: [] }, { + given: null, + original: target, + } as any); } describe('schema', () => { @@ -62,7 +60,7 @@ describe('schema', () => { expect(runSchemaPath(target, fieldToCheck, path)).toEqual([ { path: ['example'], - message: `"example" property should have required property 'url'`, + message: 'object should have required property `url`', }, ]); }); @@ -78,7 +76,7 @@ describe('schema', () => { expect(runSchemaPath(target, fieldToCheck, path)).toEqual([ { - message: '"example" property format should match format "url"', + message: '{{property|gravis|append-property|optional-typeof}}format should match format `url`', path: ['example'], }, ]); diff --git a/src/functions/__tests__/schema.test.ts b/src/functions/__tests__/schema.test.ts index 8a59e2fd1..54b128c18 100644 --- a/src/functions/__tests__/schema.test.ts +++ b/src/functions/__tests__/schema.test.ts @@ -2,7 +2,7 @@ import { JSONSchema4, JSONSchema6 } from 'json-schema'; import { schema } from '../schema'; function runSchema(target: any, schemaObj: object) { - return schema(target, { schema: schemaObj }, { given: [] }, { given: null, original: null }); + return schema(target, { schema: schemaObj }, { given: [] }, { given: null, original: null } as any); } describe('schema', () => { @@ -14,7 +14,7 @@ describe('schema', () => { expect(runSchema('', testSchema)).toEqual([ { - message: 'type should be number', + message: '{{property|gravis|append-property|optional-typeof}}type should be number', path: [], }, ]); @@ -27,7 +27,7 @@ describe('schema', () => { expect(runSchema(0, testSchema)).toEqual([ { - message: 'type should be string', + message: `{{property|gravis|append-property|optional-typeof}}type should be string`, path: [], }, ]); @@ -40,7 +40,7 @@ describe('schema', () => { expect(runSchema(false, testSchema)).toEqual([ { - message: 'type should be string', + message: `{{property|gravis|append-property|optional-typeof}}type should be string`, path: [], }, ]); @@ -53,7 +53,7 @@ describe('schema', () => { expect(runSchema(null, testSchema)).toEqual([ { - message: 'type should be string', + message: `{{property|gravis|append-property|optional-typeof}}type should be string`, path: [], }, ]); @@ -94,7 +94,7 @@ describe('schema', () => { const input = { foo: 'bar' }; expect(runSchema(input, testSchema)).toEqual([ expect.objectContaining({ - message: 'type should be array', + message: '{{property|gravis|append-property|optional-typeof}}type should be array', path: [], }), ]); @@ -104,7 +104,7 @@ describe('schema', () => { const input = ['1', '2']; expect(runSchema(input, testSchema)).toEqual([ expect.objectContaining({ - message: 'maxItems should NOT have more than 1 items', + message: '{{property|gravis|append-property|optional-typeof}}maxItems should NOT have more than 1 items', path: [], }), ]); @@ -140,7 +140,7 @@ describe('schema', () => { ), ).toEqual([ { - message: 'type should be string', + message: `{{property|gravis|append-property|optional-typeof}}type should be string`, path: ['foo', 'bar'], }, ]); @@ -157,7 +157,7 @@ describe('schema', () => { ), ).toEqual([ { - message: '/foo Property baz is not expected to be here', + message: 'property baz is not expected to be here', path: ['foo'], }, ]); @@ -174,7 +174,7 @@ describe('schema', () => { const input = 'not an email'; expect(runSchema(input, testSchema)).toEqual([ expect.objectContaining({ - message: 'format should match format "email"', + message: '{{property|gravis|append-property|optional-typeof}}format should match format `email`', path: [], }), ]); @@ -212,7 +212,7 @@ describe('schema', () => { expect(runSchema(2, testSchema)).toEqual([ { path: [], - message: 'type should be string', + message: `{{property|gravis|append-property|optional-typeof}}type should be string`, }, ]); expect(runSchema('a', testSchema2)).toEqual([]); @@ -232,7 +232,7 @@ describe('schema', () => { expect(runSchema(2, testSchema)).toEqual([ { path: [], - message: 'type should be string', + message: `{{property|gravis|append-property|optional-typeof}}type should be string`, }, ]); expect(runSchema('a', testSchema2)).toEqual([]); @@ -258,7 +258,7 @@ describe('schema', () => { it('reports pretty enum errors for a string', () => { expect(runSchema('baz', testSchema)).toEqual([ { - message: 'should be equal to one of the allowed values: foo, bar. Did you mean bar?', + message: `string should be equal to one of the allowed values: foo, bar. Did you mean bar?`, path: [], }, ]); @@ -267,7 +267,7 @@ describe('schema', () => { it('reports pretty enum errors for a number', () => { expect(runSchema(2, testSchema)).toEqual([ { - message: 'type should be string', + message: `{{property|gravis|append-property|optional-typeof}}type should be string`, path: [], }, ]); @@ -284,7 +284,7 @@ describe('schema', () => { it('reports pretty enum errors for a string', () => { expect(runSchema('baz', testSchema)).toEqual([ { - message: 'type should be integer', + message: '{{property|gravis|append-property|optional-typeof}}type should be integer', path: [], }, ]); @@ -293,7 +293,7 @@ describe('schema', () => { it('reports pretty enum errors for a number', () => { expect(runSchema(2, testSchema)).toEqual([ { - message: 'should be equal to one of the allowed values: 1, 3, 5, 10, 12', + message: `number should be equal to one of the allowed values: 1, 3, 5, 10, 12`, path: [], }, ]); @@ -310,7 +310,7 @@ describe('schema', () => { expect(runSchema('three', testSchema)).toEqual([ { - message: 'should be equal to one of the allowed values: foo, bar', + message: `string should be equal to one of the allowed values: foo, bar`, path: [], }, ]); diff --git a/src/functions/__tests__/truthy.test.ts b/src/functions/__tests__/truthy.test.ts index 86893dd23..f31800e2d 100644 --- a/src/functions/__tests__/truthy.test.ts +++ b/src/functions/__tests__/truthy.test.ts @@ -11,7 +11,7 @@ function runTruthy(targetVal: any, targetPath?: any) { { given: null, original: null, - }, + } as any, ); } @@ -23,7 +23,7 @@ describe('truthy', () => { test('should return an error message if target value is falsy', () => { expect(runTruthy(false)).toEqual([ { - message: 'property is not truthy', + message: '{{property|gravis|append-property}}is not truthy', }, ]); }); @@ -31,15 +31,7 @@ describe('truthy', () => { test('should return an error message if target value is null', () => { expect(runTruthy(null)).toEqual([ { - message: 'property is not truthy', - }, - ]); - }); - - test('should return a detailed error message if target path is set', () => { - expect(runTruthy(null, ['a', 'b'])).toEqual([ - { - message: 'a.b is not truthy', + message: '{{property|gravis|append-property}}is not truthy', }, ]); }); diff --git a/src/functions/__tests__/unreferencedReusableObject.test.ts b/src/functions/__tests__/unreferencedReusableObject.test.ts new file mode 100644 index 000000000..c9892f127 --- /dev/null +++ b/src/functions/__tests__/unreferencedReusableObject.test.ts @@ -0,0 +1,18 @@ +import { unreferencedReusableObject } from '../unreferencedReusableObject'; + +function runUnreferencedReusableObject(data: any, reusableObjectsLocation: string) { + return unreferencedReusableObject(data, { reusableObjectsLocation }, { given: ['$'] }, { + given: null, + original: null, + } as any); +} + +describe('unreferencedReusableObject', () => { + test('throws when reusableObjectsLocation does not look like a valid local json pointer', () => { + expect(() => runUnreferencedReusableObject({}, 'Nope')).toThrow(); + }); + + test('given a non object data should return empty array', () => { + expect(runUnreferencedReusableObject('Nope', '#')).toEqual([]); + }); +}); diff --git a/src/functions/casing.ts b/src/functions/casing.ts new file mode 100644 index 000000000..223391b10 --- /dev/null +++ b/src/functions/casing.ts @@ -0,0 +1,40 @@ +import { Dictionary } from '@stoplight/types'; +import { ICasingOptions, IFunction } from '../types'; + +const CASES: Dictionary = { + flat: /^[a-z]+$/, + camel: /^[a-z]+(?:[A-Z][a-z]+)*$/, + pascal: /^[A-Z][a-z]+(?:[A-Z][a-z]+)*$/, + kebab: /^[a-z]+(?:-[a-z]+)*$/, + cobol: /^[A-Z]+(?:-[A-Z]+)*$/, + snake: /^[a-z]+(?:_[a-z]+)*$/, + macro: /^[A-Z]+(?:_[A-Z]+)*$/, +}; + +const CASES_WITH_DIGITS: Dictionary = { + flat: /^[a-z][a-z0-9]*$/, + camel: /^[a-z][a-z0-9]*(?:[A-Z0-9][a-z0-9]+)*$/, + pascal: /^[A-Z][a-z0-9]*(?:[A-Z0-9][a-z0-9]+)*$/, + kebab: /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/, + cobol: /^[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*$/, + snake: /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/, + macro: /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/, +}; + +export const casing: IFunction = (targetVal, opts) => { + if (typeof targetVal !== 'string' || targetVal.length === 0) { + return; + } + + const set = opts.disallowDigits ? CASES : CASES_WITH_DIGITS; + + if (opts.type in set && !set[opts.type].test(targetVal)) { + return [ + { + message: `must be ${opts.type} case`, + }, + ]; + } + + return; +}; diff --git a/src/functions/falsy.ts b/src/functions/falsy.ts index 2d7febc22..30c50d5f9 100644 --- a/src/functions/falsy.ts +++ b/src/functions/falsy.ts @@ -1,10 +1,10 @@ import { IFunction, IFunctionResult } from '../types'; -export const falsy: IFunction = (targetVal, _opts, paths): void | IFunctionResult[] => { +export const falsy: IFunction = (targetVal): void | IFunctionResult[] => { if (!!targetVal) { return [ { - message: `${paths.target ? paths.target.join('.') : 'property'} is not falsy`, + message: '{{property|gravis|append-property}}is not falsy', }, ]; } diff --git a/src/functions/index.ts b/src/functions/index.ts index 8d8d67719..49c4dde4a 100644 --- a/src/functions/index.ts +++ b/src/functions/index.ts @@ -1,5 +1,6 @@ export const functions = { alphabetical: require('./alphabetical').alphabetical, + casing: require('./casing').casing, enumeration: require('./enumeration').enumeration, length: require('./length').length, pattern: require('./pattern').pattern, @@ -9,4 +10,5 @@ export const functions = { truthy: require('./truthy').truthy, undefined: require('./undefined').undefined, xor: require('./xor').xor, + unreferencedReusableObject: require('./unreferencedReusableObject').unreferencedReusableObject, }; diff --git a/src/functions/schema-path.ts b/src/functions/schema-path.ts index ae11900a6..e8843166e 100644 --- a/src/functions/schema-path.ts +++ b/src/functions/schema-path.ts @@ -21,9 +21,6 @@ export const schemaPath: IFunction = (targetVal, opts, paths const relevantObject = opts.field ? object[opts.field] : object; if (!relevantObject) return []; const { target, given } = paths; - const lastItem = target - ? target.length > 0 && target[target.length - 1] - : given.length > 0 && given[given.length - 1]; // The subsection of the targetValue which contains the schema for us to validate the good bit against let schemaObject; @@ -40,15 +37,5 @@ export const schemaPath: IFunction = (targetVal, opts, paths } } - const errors = schema(relevantObject, { schema: schemaObject }, paths, otherValues); - return ( - errors && - errors.map(error => { - const propertyPath = [lastItem, opts.field].filter(Boolean).join('.'); - return { - ...error, - message: `${propertyPath ? `"${propertyPath}"` : ''} property ${error.message}`, - }; - }) - ); + return schema(relevantObject, { schema: schemaObject }, paths, otherValues); }; diff --git a/src/functions/schema.ts b/src/functions/schema.ts index 3b2055a41..058702b2f 100644 --- a/src/functions/schema.ts +++ b/src/functions/schema.ts @@ -6,9 +6,8 @@ import * as jsonSpecv4 from 'ajv/lib/refs/json-schema-draft-04.json'; import * as jsonSpecv6 from 'ajv/lib/refs/json-schema-draft-06.json'; import * as jsonSpecv7 from 'ajv/lib/refs/json-schema-draft-07.json'; import { IOutputError } from 'better-ajv-errors'; -import { JSONSchema4, JSONSchema6 } from 'json-schema'; import { escapeRegExp } from 'lodash'; -import { IFunction, IFunctionResult, ISchemaOptions } from '../types'; +import { IFunction, IFunctionResult, ISchemaOptions, JSONSchema } from '../types'; const oasFormatValidator = require('ajv-oai/lib/format-validator'); const betterAjvErrors = require('better-ajv-errors/lib/modern'); @@ -49,7 +48,7 @@ ajv.addFormat('float', { type: 'number', validate: oasFormatValidator.float }); ajv.addFormat('double', { type: 'number', validate: oasFormatValidator.double }); ajv.addFormat('byte', { type: 'string', validate: oasFormatValidator.byte }); -function getSchemaId(schemaObj: JSONSchema4 | JSONSchema6): void | string { +function getSchemaId(schemaObj: JSONSchema): void | string { if ('$id' in schemaObj) { return schemaObj.$id; } @@ -59,8 +58,8 @@ function getSchemaId(schemaObj: JSONSchema4 | JSONSchema6): void | string { } } -const validators = new class extends WeakMap { - public get(schemaObj: JSONSchema4 | JSONSchema6) { +const validators = new (class extends WeakMap { + public get(schemaObj: JSONSchema) { const schemaId = getSchemaId(schemaObj); let validator = schemaId !== void 0 ? ajv.getSchema(schemaId) : void 0; if (validator !== void 0) { @@ -76,12 +75,33 @@ const validators = new class extends WeakMap, suggestion: Optional) => { - const cleanMessage = - typeof path === 'string' ? message.trim().replace(new RegExp(`^${escapeRegExp(path)}:\\s*`), '') : message.trim(); - return `${cleanMessage}${typeof suggestion === 'string' && suggestion.length > 0 ? `. ${suggestion}` : ''}`; +const replaceProperty = (substring: string, group1: string) => { + if (group1) { + return 'property '; + } + + return '{{property|gravis|append-property|optional-typeof}}'; +}; + +const cleanAJVErrorMessage = (message: string, path: Optional, suggestion: Optional, type: string) => { + let cleanMessage = message.trim(); + + if (path) { + cleanMessage = message.replace( + new RegExp(`^${escapeRegExp(decodePointerFragment(path))}:?\\s*(Property\\s+)?`), + replaceProperty, + ); + } else if (cleanMessage.startsWith(':')) { + cleanMessage = cleanMessage.replace(/:\s*/, replaceProperty); + } else { + cleanMessage = `${type} ${cleanMessage}`; + } + + return `${cleanMessage.replace(/['"]/g, '`')}${ + typeof suggestion === 'string' && suggestion.length > 0 ? `. ${suggestion}` : '' + }`; }; export const schema: IFunction = (targetVal, opts, paths) => { @@ -93,7 +113,7 @@ export const schema: IFunction = (targetVal, opts, paths) => { return [ { path, - message: `${paths ? path.join('.') : 'property'} does not exist`, + message: `{{property|double-quotes|append-property}}does not exist`, }, ]; @@ -108,7 +128,7 @@ export const schema: IFunction = (targetVal, opts, paths) => { results.push( ...(betterAjvErrors(schemaObj, targetVal, validator.errors, { format: 'js' }) as IAJVOutputError[]).map( ({ suggestion, error, path: errorPath }) => ({ - message: cleanAJVErrorMessage(error, errorPath, suggestion), + message: cleanAJVErrorMessage(error, errorPath, suggestion, typeof targetVal), path: [...path, ...(errorPath ? errorPath.replace(/^\//, '').split('/') : [])], }), ), @@ -116,7 +136,7 @@ export const schema: IFunction = (targetVal, opts, paths) => { } catch { results.push( ...validator.errors.map(({ message, dataPath }) => ({ - message: message ? cleanAJVErrorMessage(message, dataPath, void 0) : '', + message: message ? cleanAJVErrorMessage(message, dataPath, void 0, typeof targetVal) : '', path: [ ...path, ...dataPath diff --git a/src/functions/truthy.ts b/src/functions/truthy.ts index e7bae6395..e3a2a024c 100644 --- a/src/functions/truthy.ts +++ b/src/functions/truthy.ts @@ -1,10 +1,10 @@ import { IFunction, IFunctionResult } from '../types'; -export const truthy: IFunction = (targetVal, _opts, paths): void | IFunctionResult[] => { +export const truthy: IFunction = (targetVal): void | IFunctionResult[] => { if (!targetVal) { return [ { - message: `${paths.target ? paths.target.join('.') : 'property'} is not truthy`, + message: '{{property|gravis|append-property}}is not truthy', }, ]; } diff --git a/src/functions/undefined.ts b/src/functions/undefined.ts index d2352268b..e6f5e869d 100644 --- a/src/functions/undefined.ts +++ b/src/functions/undefined.ts @@ -1,10 +1,10 @@ import { IFunction, IFunctionResult } from '../types'; -export const undefined: IFunction = (targetVal, _opts, paths): void | IFunctionResult[] => { +export const undefined: IFunction = (targetVal): void | IFunctionResult[] => { if (typeof targetVal !== 'undefined') { return [ { - message: `${paths.target ? paths.target.join('.') : 'property'} should be undefined`, + message: '{{property|gravis|append-property}}should be undefined', }, ]; } diff --git a/src/functions/unreferencedReusableObject.ts b/src/functions/unreferencedReusableObject.ts new file mode 100644 index 000000000..35e213a14 --- /dev/null +++ b/src/functions/unreferencedReusableObject.ts @@ -0,0 +1,30 @@ +import { IFunction } from '../types'; +import { isObject, safePointerToPath } from '../utils'; + +export const unreferencedReusableObject: IFunction<{ reusableObjectsLocation: string }> = ( + data, + opts, + _paths, + otherValues, +) => { + if (!isObject(data)) return []; + + if (!opts.reusableObjectsLocation.startsWith('#')) { + throw new Error( + "Function option 'reusableObjectsLocation' doesn't look like containing a valid local json pointer.", + ); + } + + const normalizedSource = otherValues.resolved.source ?? ''; + + const defined = Object.keys(data).map(name => `${normalizedSource}${opts.reusableObjectsLocation}/${name}`); + + const orphans = defined.filter(defPath => !otherValues.resolved.graph.hasNode(defPath)); + + return orphans.map(orphanPath => { + return { + message: 'Potential orphaned reusable object has been detected.', + path: safePointerToPath(orphanPath), + }; + }); +}; diff --git a/src/functions/xor.ts b/src/functions/xor.ts index 823e0da48..a3205a03d 100644 --- a/src/functions/xor.ts +++ b/src/functions/xor.ts @@ -10,7 +10,7 @@ export const xor: IFunction = (targetVal, opts) => { const intersection = Object.keys(targetVal).filter(value => -1 !== properties.indexOf(value)); if (intersection.length !== 1) { results.push({ - message: `${properties[0]} and ${properties[1]} cannot both be defined`, + message: `${properties[0]} and ${properties[1]} cannot be both defined or both undefined`, }); } diff --git a/src/index.ts b/src/index.ts index 6463d20dc..6447c6556 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export * from './spectral'; export * from './formats'; +export * from './parsers'; diff --git a/src/linter.ts b/src/linter.ts index de25f634b..899b1f7b1 100644 --- a/src/linter.ts +++ b/src/linter.ts @@ -1,14 +1,12 @@ -import { JsonPath } from '@stoplight/types'; -import { get, has } from 'lodash'; - -const { JSONPath } = require('jsonpath-plus'); - -import { decodePointerFragment, pathToPointer } from '@stoplight/json'; -import { Resolved } from './resolved'; -import { message } from './rulesets/message'; +import { decodePointerFragment } from '@stoplight/json'; +import { get } from 'lodash'; +import { getDefaultRange, Resolved } from './resolved'; +import { IMessageVars, message } from './rulesets/message'; import { getDiagnosticSeverity } from './rulesets/severity'; import { IFunction, IGivenNode, IRuleResult, IRunRule, IThen } from './types'; -import { isObject } from './utils'; +import { getClosestJsonPath, printPath, PrintStyle } from './utils'; + +const { JSONPath } = require('jsonpath-plus'); // TODO(SO-23): unit test but mock whatShouldBeLinted export const lintNode = ( @@ -19,14 +17,7 @@ export const lintNode = ( resolved: Resolved, ): IRuleResult[] => { const givenPath = node.path[0] === '$' ? node.path.slice(1) : node.path; - const conditioning = whatShouldBeLinted(givenPath, node.value, rule); - - // If the 'when' condition is not satisfied, simply don't run the linter - if (!conditioning.lint) { - return []; - } - - const targetValue = conditioning.value; + const targetValue = node.value; const targets: any[] = []; if (then && then.field) { @@ -75,7 +66,7 @@ export const lintNode = ( }); } - let results: IRuleResult[] = []; + const results: IRuleResult[] = []; for (const target of targets) { const targetPath = givenPath.concat(target.path); @@ -91,44 +82,42 @@ export const lintNode = ( { original: node.value, given: node.value, + resolved, }, ) || []; - results = results.concat( - targetResults.map(result => { + results.push( + ...targetResults.map(result => { const escapedJsonPath = (result.path || targetPath).map(segment => decodePointerFragment(String(segment))); - const path = getClosestJsonPath( - rule.resolved === false ? resolved.unresolved : resolved.resolved, - escapedJsonPath, - ); - // todo: https://github.com/stoplightio/spectral/issues/608 - const location = resolved.getLocationForJsonPath(path, true); + const parsed = resolved.getParsedForJsonPath(escapedJsonPath); + const path = parsed?.path || getClosestJsonPath(resolved.resolved, escapedJsonPath); + const doc = parsed?.doc || resolved.parsed; + const range = doc.getLocationForJsonPath(doc.parsed, path, true)?.range || getDefaultRange(); + const value = path.length === 0 ? parsed?.doc.parsed.data : get(parsed?.doc.parsed.data, path); + + const vars: IMessageVars = { + property: + parsed?.missingPropertyPath && parsed.missingPropertyPath.length > path.length + ? printPath(parsed.missingPropertyPath.slice(path.length - 1), PrintStyle.Dot) + : path.length > 0 + ? path[path.length - 1] + : '', + error: result.message, + path: printPath(path, PrintStyle.EscapedPointer), + description: rule.description, + value, + }; + + const resultMessage = message(result.message, vars); + vars.error = resultMessage; return { code: rule.name, - - message: - rule.message === undefined - ? rule.description || result.message - : message(rule.message, { - error: result.message, - property: path.length > 0 ? path[path.length - 1] : '', - path: pathToPointer(path), - description: rule.description, - get value() { - // let's make it `value` lazy - const value = resolved.getValueForJsonPath(path); - if (isObject(value)) { - return Array.isArray(value) ? 'Array[]' : 'Object{}'; - } - - return JSON.stringify(value); - }, - }), + message: (rule.message === void 0 ? rule.description ?? resultMessage : message(rule.message, vars)).trim(), path, severity: getDiagnosticSeverity(rule.severity), - source: location.uri, - range: location.range, + source: parsed?.doc.source, + range, }; }), ); @@ -136,88 +125,3 @@ export const lintNode = ( return results; }; - -// TODO(SO-23): unit test idividually -export const whatShouldBeLinted = ( - path: JsonPath, - originalValue: any, - rule: IRunRule, -): { lint: boolean; value: any } => { - const leaf = path[path.length - 1]; - - const when = rule.when; - if (!when) { - return { - lint: true, - value: originalValue, - }; - } - - const pattern = when.pattern; - const field = when.field; - - // TODO: what if someone's field is called '@key'? should we use @@key? - const isKey = field === '@key'; - - if (!pattern) { - // isKey doesn't make sense without pattern - if (isKey) { - return { - lint: false, - value: originalValue, - }; - } - - return { - lint: has(originalValue, field), - value: originalValue, - }; - } - - if (isKey && pattern) { - return keyAndOptionalPattern(leaf, pattern, originalValue); - } - - const fieldValue = String(get(originalValue, when.field)); - - return { - lint: fieldValue.match(pattern) !== null, - value: originalValue, - }; -}; - -function keyAndOptionalPattern(key: string | number, pattern: string, value: any) { - /** arrays, look at the keys on the array object. note, this number check on id prop is not foolproof... */ - if (typeof key === 'number' && typeof value === 'object') { - for (const k of Object.keys(value)) { - if (String(k).match(pattern)) { - return { - lint: true, - value, - }; - } - } - } else if (String(key).match(pattern)) { - // objects - return { - lint: true, - value, - }; - } - - return { - lint: false, - value, - }; -} - -// todo: revisit -> https://github.com/stoplightio/spectral/issues/608 -function getClosestJsonPath(data: unknown, path: JsonPath) { - if (data === null || typeof data !== 'object') return []; - - while (path.length > 0 && !has(data, path)) { - path.pop(); - } - - return path; -} diff --git a/src/meta/rule.schema.json b/src/meta/rule.schema.json index 5963e09d3..5dbd76129 100644 --- a/src/meta/rule.schema.json +++ b/src/meta/rule.schema.json @@ -66,7 +66,17 @@ "type": "boolean" }, "given": { - "type": "string" + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] }, "resolved": { "type": "boolean" @@ -109,20 +119,6 @@ "validation" ], "type": "string" - }, - "when": { - "properties": { - "field": { - "type": "string" - }, - "pattern": { - "type": "string" - } - }, - "required": [ - "field" - ], - "type": "object" } }, "required": [ diff --git a/src/meta/ruleset.schema.json b/src/meta/ruleset.schema.json index 7d9b3ec1d..9cab5e712 100644 --- a/src/meta/ruleset.schema.json +++ b/src/meta/ruleset.schema.json @@ -66,7 +66,12 @@ } } }, - "required": [ - "rules" + "anyOf": [ + { + "required": ["extends"] + }, + { + "required": ["rules"] + } ] } diff --git a/src/parsers/index.ts b/src/parsers/index.ts new file mode 100644 index 000000000..7f033cf3c --- /dev/null +++ b/src/parsers/index.ts @@ -0,0 +1,2 @@ +export * from './json'; +export * from './yaml'; diff --git a/src/parsers/json.ts b/src/parsers/json.ts new file mode 100644 index 000000000..03a7bb7bb --- /dev/null +++ b/src/parsers/json.ts @@ -0,0 +1,4 @@ +import { parseWithPointers } from '@stoplight/json'; + +export const parseJson = (input: string) => + parseWithPointers(input, { ignoreDuplicateKeys: false, preserveKeyOrder: true }); diff --git a/src/parsers/yaml.ts b/src/parsers/yaml.ts new file mode 100644 index 000000000..b4b579f0a --- /dev/null +++ b/src/parsers/yaml.ts @@ -0,0 +1,8 @@ +import { parseWithPointers } from '@stoplight/yaml'; + +export const parseYaml = (input: string) => + parseWithPointers(input, { + ignoreDuplicateKeys: false, + mergeKeys: true, + preserveKeyOrder: true, + }); diff --git a/src/resolved.ts b/src/resolved.ts index 37027bd4e..8aeaaae9a 100644 --- a/src/resolved.ts +++ b/src/resolved.ts @@ -1,12 +1,13 @@ -import { decodePointerFragment, pointerToPath } from '@stoplight/json'; -import { IResolveError } from '@stoplight/json-ref-resolver/types'; -import { Dictionary, ILocation, IRange, JsonPath, Segment } from '@stoplight/types'; +import { extractSourceFromRef, hasRef, isLocalRef } from '@stoplight/json'; +import { IGraphNodeData, IResolveError } from '@stoplight/json-ref-resolver/types'; +import { normalize, resolve } from '@stoplight/path'; +import { Dictionary, ILocation, IRange, JsonPath } from '@stoplight/types'; +import { DepGraph } from 'dependency-graph'; import { get } from 'lodash'; -import { IParseMap, REF_METADATA, ResolveResult } from './spectral'; -import { IParsedResult } from './types'; -import { hasRef, isObject } from './utils'; +import { IParsedResult, ResolveResult } from './types'; +import { getClosestJsonPath, getEndRef, isAbsoluteRef, safePointerToPath, traverseObjUntilRef } from './utils'; -const getDefaultRange = (): IRange => ({ +export const getDefaultRange = (): IRange => ({ start: { line: 0, character: 0, @@ -19,72 +20,85 @@ const getDefaultRange = (): IRange => ({ export class Resolved { public readonly refMap: Dictionary; + public readonly graph: DepGraph; public readonly resolved: unknown; public readonly unresolved: unknown; public readonly errors: IResolveError[]; public formats?: string[] | null; - constructor(public spec: IParsedResult, resolveResult: ResolveResult, public parsedMap: IParseMap) { - this.unresolved = spec.parsed.data; - this.formats = spec.formats; + public get source() { + return this.parsed.source ? normalize(this.parsed.source) : this.parsed.source; + } + + constructor( + public readonly parsed: IParsedResult, + resolveResult: ResolveResult, + public parsedRefs: Dictionary, + ) { + this.unresolved = parsed.parsed.data; + this.formats = parsed.formats; this.refMap = resolveResult.refMap; + this.graph = resolveResult.graph; this.resolved = resolveResult.result; this.errors = resolveResult.errors; } - public doesBelongToDoc(path: JsonPath): boolean { - if (path.length === 0) { - // todo: each rule and their function should be context-aware, meaning they should aware of the fact they operate on resolved content - // let's assume the error was reported correctly by any custom rule /shrug - return true; - } + public getParsedForJsonPath(path: JsonPath) { + try { + const newPath: JsonPath = getClosestJsonPath(this.resolved, path); + let $ref = traverseObjUntilRef(this.unresolved, newPath); + + if ($ref === null) { + return { + path: getClosestJsonPath(this.unresolved, path), + doc: this.parsed, + missingPropertyPath: path, + }; + } - let piece = this.unresolved; + const missingPropertyPath = + newPath.length === 0 ? [] : path.slice(path.lastIndexOf(newPath[newPath.length - 1]) + 1); - for (let i = 0; i < path.length; i++) { - if (!isObject(piece)) return false; + let { source } = this; - if (path[i] in piece) { - piece = piece[path[i]]; - } else if (hasRef(piece)) { - return this.doesBelongToDoc([...pointerToPath(piece.$ref), ...path.slice(i)]); - } - } + while (true) { + if (source === void 0) return null; - return true; - } + $ref = getEndRef(this.graph.getNodeData(source).refMap, $ref); - public getParsedForJsonPath(path: JsonPath) { - let target: object = this.parsedMap.refs; - const newPath = [...path]; - let segment: Segment; - - while (newPath.length > 0) { - segment = newPath.shift()!; - if (segment && segment in target) { - target = target[segment]; - } else { - newPath.unshift(segment); - break; - } - } + if ($ref === null) return null; - if (target && target[REF_METADATA]) { - return { - path: [...get(target, [REF_METADATA, 'root'], []).map(decodePointerFragment), ...newPath], - doc: get(this.parsedMap.parsed, get(target, [REF_METADATA, 'ref']), this.spec), - }; - } + const scopedPath = [...safePointerToPath($ref), ...newPath]; + let resolvedDoc; + + if (isLocalRef($ref)) { + resolvedDoc = source === this.parsed.source ? this.parsed : this.parsedRefs[source]; + } else { + const extractedSource = extractSourceFromRef($ref)!; + source = isAbsoluteRef(extractedSource) ? extractedSource : resolve(source, '..', extractedSource); + + resolvedDoc = source === this.parsed.source ? this.parsed : this.parsedRefs[source]; + const { parsed } = resolvedDoc; + + const obj = scopedPath.length === 0 || hasRef(parsed.data) ? parsed.data : get(parsed.data, scopedPath); - if (!this.doesBelongToDoc(path)) { + if (hasRef(obj)) { + $ref = obj.$ref; + continue; + } + } + + const closestPath = getClosestJsonPath(resolvedDoc.parsed.data, scopedPath); + return { + doc: resolvedDoc, + path: closestPath, + missingPropertyPath: [...closestPath, ...missingPropertyPath], + }; + } + } catch { return null; } - - return { - path, - doc: this.spec, - }; } public getLocationForJsonPath(path: JsonPath, closest?: boolean): ILocation { @@ -99,7 +113,7 @@ export class Resolved { return { ...(parsedResult.doc.source && { uri: parsedResult.doc.source }), - range: location !== void 0 ? location.range : getDefaultRange(), + range: location?.range || getDefaultRange(), }; } diff --git a/src/resolvers/http-and-file.ts b/src/resolvers/http-and-file.ts index 1a0378282..a38c92528 100644 --- a/src/resolvers/http-and-file.ts +++ b/src/resolvers/http-and-file.ts @@ -1,26 +1,14 @@ +import { createResolveHttp, resolveFile } from '@stoplight/json-ref-readers'; import { Resolver } from '@stoplight/json-ref-resolver'; -import * as fs from 'fs'; +import { DEFAULT_REQUEST_OPTIONS } from '../request'; -import { httpReader } from './http'; +const resolveHttp = createResolveHttp(DEFAULT_REQUEST_OPTIONS); // resolves files, http and https $refs, and internal $refs export const httpAndFileResolver = new Resolver({ resolvers: { - https: httpReader, - http: httpReader, - file: { - resolve(ref: any) { - return new Promise((resolve, reject) => { - const path = ref.path(); - fs.readFile(path, 'utf8', (err, data) => { - if (err) { - reject(err); - } else { - resolve(data); - } - }); - }); - }, - }, + https: { resolve: resolveHttp }, + http: { resolve: resolveHttp }, + file: { resolve: resolveFile }, }, }); diff --git a/src/resolvers/http.ts b/src/resolvers/http.ts deleted file mode 100644 index eacdc627e..000000000 --- a/src/resolvers/http.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Resolver } from '@stoplight/json-ref-resolver'; - -import request from '../request'; - -export const httpReader = { - async resolve(ref: any) { - return (await request(String(ref))).text(); - }, -}; - -// resolves http and https $refs, and internal $refs -export const httpResolver = new Resolver({ - resolvers: { - https: httpReader, - http: httpReader, - }, -}); diff --git a/src/rulesets/__tests__/__fixtures__/custom-oas-ruleset.json b/src/rulesets/__tests__/__fixtures__/custom-oas-ruleset.json new file mode 100644 index 000000000..feaa37e0b --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/custom-oas-ruleset.json @@ -0,0 +1,12 @@ +{ + "formats": ["oas2", "oas3"], + "rules": { + "generic-valid-rule": { + "message": "should be OK", + "given": "$.info", + "then": { + "function": "truthy" + } + } + } +} diff --git a/src/rulesets/__tests__/__fixtures__/custom-oas2-ruleset.json b/src/rulesets/__tests__/__fixtures__/custom-oas2-ruleset.json new file mode 100644 index 000000000..de8094de7 --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/custom-oas2-ruleset.json @@ -0,0 +1,12 @@ +{ + "formats": ["oas2"], + "rules": { + "oas2-valid-rule": { + "message": "should be OK", + "given": "$.info", + "then": { + "function": "truthy" + } + } + } +} diff --git a/src/rulesets/__tests__/__fixtures__/custom-oas3-ruleset.json b/src/rulesets/__tests__/__fixtures__/custom-oas3-ruleset.json new file mode 100644 index 000000000..82714db5b --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/custom-oas3-ruleset.json @@ -0,0 +1,12 @@ +{ + "formats": ["oas3"], + "rules": { + "oas3-valid-rule": { + "message": "should be OK", + "given": "$.info", + "then": { + "function": "truthy" + } + } + } +} diff --git a/src/rulesets/__tests__/__fixtures__/enable-all-ruleset.json b/src/rulesets/__tests__/__fixtures__/enable-all-ruleset.json index f1feac770..70b11ff3f 100644 --- a/src/rulesets/__tests__/__fixtures__/enable-all-ruleset.json +++ b/src/rulesets/__tests__/__fixtures__/enable-all-ruleset.json @@ -1,6 +1,6 @@ { "extends": [[ - "./extends-disabled-oas2-ruleset.yaml", "all" + "./extends-disabled-oas-ruleset.yaml", "all" ]], "rules": {} } diff --git a/src/rulesets/__tests__/__fixtures__/extends-disabled-oas2-ruleset.yaml b/src/rulesets/__tests__/__fixtures__/extends-disabled-oas-ruleset.yaml similarity index 57% rename from src/rulesets/__tests__/__fixtures__/extends-disabled-oas2-ruleset.yaml rename to src/rulesets/__tests__/__fixtures__/extends-disabled-oas-ruleset.yaml index dded304ee..af31524bf 100644 --- a/src/rulesets/__tests__/__fixtures__/extends-disabled-oas2-ruleset.yaml +++ b/src/rulesets/__tests__/__fixtures__/extends-disabled-oas-ruleset.yaml @@ -1,3 +1,3 @@ -extends: [[spectral:oas2, off]] +extends: [[spectral:oas, off]] rules: operation-operationId-unique: true diff --git a/src/rulesets/__tests__/__fixtures__/extends-oas2-ruleset.json b/src/rulesets/__tests__/__fixtures__/extends-oas-ruleset.json similarity index 79% rename from src/rulesets/__tests__/__fixtures__/extends-oas2-ruleset.json rename to src/rulesets/__tests__/__fixtures__/extends-oas-ruleset.json index 66efb24d9..e59b70328 100644 --- a/src/rulesets/__tests__/__fixtures__/extends-oas2-ruleset.json +++ b/src/rulesets/__tests__/__fixtures__/extends-oas-ruleset.json @@ -1,5 +1,5 @@ { - "extends": [["spectral:oas2", "all"]], + "extends": [["spectral:oas", "all"]], "rules": { "valid-rule": { "message": "should be OK", diff --git a/src/rulesets/__tests__/__fixtures__/extends-oas2-with-override-ruleset.json b/src/rulesets/__tests__/__fixtures__/extends-oas-with-override-ruleset.json similarity index 86% rename from src/rulesets/__tests__/__fixtures__/extends-oas2-with-override-ruleset.json rename to src/rulesets/__tests__/__fixtures__/extends-oas-with-override-ruleset.json index f0a0db089..47b6f8566 100644 --- a/src/rulesets/__tests__/__fixtures__/extends-oas2-with-override-ruleset.json +++ b/src/rulesets/__tests__/__fixtures__/extends-oas-with-override-ruleset.json @@ -1,5 +1,5 @@ { - "extends": [["spectral:oas2", "all"]], + "extends": [["spectral:oas", "all"]], "rules": { "oas2-operation-security-defined": "off", "operation-2xx-response": { diff --git a/src/rulesets/__tests__/__fixtures__/extends-only-ruleset.json b/src/rulesets/__tests__/__fixtures__/extends-only-ruleset.json new file mode 100644 index 000000000..d5f16a728 --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/extends-only-ruleset.json @@ -0,0 +1,6 @@ +{ + "extends": [ + "./custom-functions-directory-ruleset.json", + "./foo-ruleset.json" + ] +} diff --git a/src/rulesets/__tests__/__fixtures__/extends-unspecified-oas2-ruleset.json b/src/rulesets/__tests__/__fixtures__/extends-unspecified-oas-ruleset.json similarity index 83% rename from src/rulesets/__tests__/__fixtures__/extends-unspecified-oas2-ruleset.json rename to src/rulesets/__tests__/__fixtures__/extends-unspecified-oas-ruleset.json index 6df464f1d..a1cb1cc4d 100644 --- a/src/rulesets/__tests__/__fixtures__/extends-unspecified-oas2-ruleset.json +++ b/src/rulesets/__tests__/__fixtures__/extends-unspecified-oas-ruleset.json @@ -1,5 +1,5 @@ { - "extends": ["spectral:oas2"], + "extends": ["spectral:oas"], "rules": { "valid-rule": { "message": "should be OK", diff --git a/src/rulesets/__tests__/__fixtures__/github-issue-447-fixture.yaml b/src/rulesets/__tests__/__fixtures__/github-issue-447-fixture.yaml index 3211964c0..409464017 100644 --- a/src/rulesets/__tests__/__fixtures__/github-issue-447-fixture.yaml +++ b/src/rulesets/__tests__/__fixtures__/github-issue-447-fixture.yaml @@ -1,4 +1,4 @@ -extends: spectral:oas3 +extends: spectral:oas rules: operation-operationId: error operation-tags: hint diff --git a/src/rulesets/__tests__/__fixtures__/inheritanceRulesets/my-ruleset-recommended.json b/src/rulesets/__tests__/__fixtures__/inheritanceRulesets/my-ruleset-recommended.json new file mode 100644 index 000000000..9c7045221 --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/inheritanceRulesets/my-ruleset-recommended.json @@ -0,0 +1,3 @@ +{ + "extends": [["./my-ruleset.json", "recommended"]] +} diff --git a/src/rulesets/__tests__/__fixtures__/inheritanceRulesets/my-ruleset.json b/src/rulesets/__tests__/__fixtures__/inheritanceRulesets/my-ruleset.json new file mode 100644 index 000000000..36a45660d --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/inheritanceRulesets/my-ruleset.json @@ -0,0 +1,3 @@ +{ + "extends": [["./ruleset-c.json", "all"]] +} diff --git a/src/rulesets/__tests__/__fixtures__/inheritanceRulesets/ruleset-a.json b/src/rulesets/__tests__/__fixtures__/inheritanceRulesets/ruleset-a.json new file mode 100644 index 000000000..1f5deb95c --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/inheritanceRulesets/ruleset-a.json @@ -0,0 +1,43 @@ +{ + "rules": { + "description-matches-stoplight": { + "message": "Description must contain Stoplight", + "given": "$.info", + "type": "style", + "recommended": true, + "severity": "error", + "then": { + "field": "description", + "function": "pattern", + "functionOptions": { + "match": "Stoplight" + } + } + }, + "title-matches-stoplight": { + "message": "Title must contain Stoplight", + "given": "$.info", + "type": "style", + "then": { + "field": "title", + "function": "pattern", + "functionOptions": { + "match": "Stoplight" + } + } + }, + "contact-name-matches-stoplight": { + "message": "Contact name must contain Stoplight", + "given": "$.info.contact", + "type": "style", + "recommended": false, + "then": { + "field": "name", + "function": "pattern", + "functionOptions": { + "match": "Stoplight" + } + } + } + } +} diff --git a/src/rulesets/__tests__/__fixtures__/inheritanceRulesets/ruleset-b.json b/src/rulesets/__tests__/__fixtures__/inheritanceRulesets/ruleset-b.json new file mode 100644 index 000000000..e577d2144 --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/inheritanceRulesets/ruleset-b.json @@ -0,0 +1,3 @@ +{ + "extends": "./ruleset-a.json" +} diff --git a/src/rulesets/__tests__/__fixtures__/inheritanceRulesets/ruleset-c.json b/src/rulesets/__tests__/__fixtures__/inheritanceRulesets/ruleset-c.json new file mode 100644 index 000000000..6ec51ead9 --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/inheritanceRulesets/ruleset-c.json @@ -0,0 +1,3 @@ +{ + "extends": [["./ruleset-b.json", "off"]] +} diff --git a/src/rulesets/__tests__/__fixtures__/my-open-api-ruleset.json b/src/rulesets/__tests__/__fixtures__/my-open-api-ruleset.json index dbbcf3374..0a2cf89b0 100644 --- a/src/rulesets/__tests__/__fixtures__/my-open-api-ruleset.json +++ b/src/rulesets/__tests__/__fixtures__/my-open-api-ruleset.json @@ -1,7 +1,11 @@ { - "extends": ["spectral:oas2", "spectral:oas3"], + "extends": [ + "./custom-oas-ruleset.json", + "./custom-oas2-ruleset.json", + "./custom-oas3-ruleset.json" + ], "rules": { - "valid-rule": { + "my-valid-rule": { "message": "should be OK", "given": "$.info", "then": { diff --git a/src/rulesets/__tests__/__fixtures__/unusedComponentsSchema.definition.json b/src/rulesets/__tests__/__fixtures__/unusedComponentsSchema.definition.json new file mode 100644 index 000000000..daa79fa72 --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/unusedComponentsSchema.definition.json @@ -0,0 +1,10 @@ +{ + "openapi": "3.0.0", + "components": { + "schemas": { + "ExternalFs": { + "type": "number" + } + } + } +} diff --git a/src/rulesets/__tests__/__fixtures__/unusedComponentsSchema.indirect.1.json b/src/rulesets/__tests__/__fixtures__/unusedComponentsSchema.indirect.1.json new file mode 100644 index 000000000..762289c5f --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/unusedComponentsSchema.indirect.1.json @@ -0,0 +1,16 @@ +{ + "openapi": "3.0.0", + "x-hook": { + "$ref": "./unusedComponentsSchema.indirect.2.json#/components/schemas/ExtHooked" + }, + "components": { + "schemas": { + "Hooked": { + "type": "object" + }, + "Unhooked": { + "type": "object" + } + } + } +} diff --git a/src/rulesets/__tests__/__fixtures__/unusedComponentsSchema.indirect.2.json b/src/rulesets/__tests__/__fixtures__/unusedComponentsSchema.indirect.2.json new file mode 100644 index 000000000..ad75258be --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/unusedComponentsSchema.indirect.2.json @@ -0,0 +1,15 @@ +{ + "components": { + "schemas": { + "ExtHooked": { + "$ref": "./unusedComponentsSchema.indirect.1.json#/components/schemas/Hooked" + }, + "ExtUnhookedOne": { + "$ref": "./unusedComponentsSchema.indirect.1.json#/components/schemas/Hooked" + }, + "ExtUnhookedTwo": { + "type": "number" + } + } + } +} diff --git a/src/rulesets/__tests__/__fixtures__/unusedComponentsSchema.remoteLocal.json b/src/rulesets/__tests__/__fixtures__/unusedComponentsSchema.remoteLocal.json new file mode 100644 index 000000000..aef838479 --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/unusedComponentsSchema.remoteLocal.json @@ -0,0 +1,19 @@ +{ + "openapi": "3.0.0", + "x-hook": { + "$ref": "./unusedComponentsSchema.remoteLocal.json#/components/schemas/RemoteLocal" + }, + "x-also-hook": { + "$ref": "#/components/schemas/PureLocal" + }, + "components": { + "schemas": { + "RemoteLocal": { + "type": "string" + }, + "PureLocal": { + "type": "string" + } + } + } +} diff --git a/src/rulesets/__tests__/__fixtures__/unusedDefinition.definition.json b/src/rulesets/__tests__/__fixtures__/unusedDefinition.definition.json new file mode 100644 index 000000000..54ce13fc8 --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/unusedDefinition.definition.json @@ -0,0 +1,8 @@ +{ + "swagger": "2.0", + "definitions": { + "ExternalFs": { + "type": "number" + } + } +} diff --git a/src/rulesets/__tests__/__fixtures__/unusedDefinition.indirect.1.json b/src/rulesets/__tests__/__fixtures__/unusedDefinition.indirect.1.json new file mode 100644 index 000000000..16a0b74b8 --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/unusedDefinition.indirect.1.json @@ -0,0 +1,14 @@ +{ + "swagger": "2.0", + "x-hook": { + "$ref": "./unusedDefinition.indirect.2.json#/definitions/ExtHooked" + }, + "definitions": { + "Hooked": { + "type": "object" + }, + "Unhooked": { + "type": "object" + } + } +} diff --git a/src/rulesets/__tests__/__fixtures__/unusedDefinition.indirect.2.json b/src/rulesets/__tests__/__fixtures__/unusedDefinition.indirect.2.json new file mode 100644 index 000000000..4e67d5a98 --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/unusedDefinition.indirect.2.json @@ -0,0 +1,13 @@ +{ + "definitions": { + "ExtHooked": { + "$ref": "./unusedDefinition.indirect.1.json#/definitions/Hooked" + }, + "ExtUnhookedOne": { + "$ref": "./unusedDefinition.indirect.1.json#/definitions/Hooked" + }, + "ExtUnhookedTwo": { + "type": "number" + } + } +} diff --git a/src/rulesets/__tests__/__fixtures__/unusedDefinition.remoteLocal.json b/src/rulesets/__tests__/__fixtures__/unusedDefinition.remoteLocal.json new file mode 100644 index 000000000..0fc6b9723 --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/unusedDefinition.remoteLocal.json @@ -0,0 +1,17 @@ +{ + "swagger": "2.0", + "x-hook": { + "$ref": "./unusedDefinition.remoteLocal.json#/definitions/RemoteLocal" + }, + "x-also-hook": { + "$ref": "#/definitions/PureLocal" + }, + "definitions": { + "RemoteLocal": { + "type": "string" + }, + "PureLocal": { + "type": "string" + } + } +} diff --git a/src/rulesets/__tests__/finder.jest.test.ts b/src/rulesets/__tests__/finder.jest.test.ts index 77d36e90c..e1f32101e 100644 --- a/src/rulesets/__tests__/finder.jest.test.ts +++ b/src/rulesets/__tests__/finder.jest.test.ts @@ -23,25 +23,22 @@ describe('Rulesets finder', () => { }); it('should support spectral built-in rules', () => { - return expect(findFile('/b/c/d', '@stoplight/spectral/rulesets/oas2/index.json')).resolves.toEqual( - path.join(process.cwd(), 'src/rulesets/oas2/index.json'), + return expect(findFile('/b/c/d', '@stoplight/spectral/rulesets/oas/index.json')).resolves.toEqual( + path.join(process.cwd(), 'src/rulesets/oas/index.json'), ); }); - it.each(['oas', 'oas2', 'oas3'])('should support spectral built-in %s ruleset shorthand', shorthand => { - return expect(findFile('', `spectral:${shorthand}`)).resolves.toEqual( - path.join(process.cwd(), `src/rulesets/${shorthand}/index.json`), + it('should support spectral built-in ruleset shorthand', () => { + return expect(findFile('', `spectral:oas`)).resolves.toEqual( + path.join(process.cwd(), `src/rulesets/oas/index.json`), ); }); - it.each(['oas', 'oas2', 'oas3'])( - 'should resolve spectral built-in %s ruleset shorthand even if a base uri is provided', - shorthand => { - return expect(findFile('https://localhost:4000', `spectral:${shorthand}`)).resolves.toEqual( - path.join(process.cwd(), `src/rulesets/${shorthand}/index.json`), - ); - }, - ); + it('should resolve spectral built-in ruleset shorthand even if a base uri is provided', () => { + return expect(findFile('https://localhost:4000', `spectral:oas`)).resolves.toEqual( + path.join(process.cwd(), `src/rulesets/oas/index.json`), + ); + }); it('should load local npm module if available', () => { return expect(findFile('', '@stoplight/json')).resolves.toEqual(require.resolve('@stoplight/json')); diff --git a/src/rulesets/__tests__/finder.karma.test.ts b/src/rulesets/__tests__/finder.karma.test.ts index c5a67bcf3..bcaecb1ae 100644 --- a/src/rulesets/__tests__/finder.karma.test.ts +++ b/src/rulesets/__tests__/finder.karma.test.ts @@ -2,22 +2,20 @@ import { findFile } from '../finder'; describe('Rulesets finder', () => { it('should point to unpkg.com if npm module', () => { - return expect(findFile('', '@stoplight/spectral/rulesets/oas2/index.json')).resolves.toEqual( - 'https://unpkg.com/@stoplight/spectral/rulesets/oas2/index.json', + return expect(findFile('', '@stoplight/spectral/rulesets/oas/index.json')).resolves.toEqual( + 'https://unpkg.com/@stoplight/spectral/rulesets/oas/index.json', ); }); - for (const shorthand of ['oas', 'oas2', 'oas3']) { - it(`should support spectral built-in ${shorthand} ruleset shorthand`, () => { - return expect(findFile('', `spectral:${shorthand}`)).resolves.toEqual( - `https://unpkg.com/@stoplight/spectral/rulesets/${shorthand}/index.json`, - ); - }); + it(`should support spectral built-in ruleset shorthand`, () => { + return expect(findFile('', `spectral:oas`)).resolves.toEqual( + `https://unpkg.com/@stoplight/spectral/rulesets/oas/index.json`, + ); + }); - it(`should resolve spectral built-in ${shorthand} ruleset shorthand even if a base uri is provided`, () => { - return expect(findFile('https://localhost:4000', `spectral:${shorthand}`)).resolves.toEqual( - `https://unpkg.com/@stoplight/spectral/rulesets/${shorthand}/index.json`, - ); - }); - } + it(`should resolve spectral built-in ruleset shorthand even if a base uri is provided`, () => { + return expect(findFile('https://localhost:4000', `spectral:oas`)).resolves.toEqual( + `https://unpkg.com/@stoplight/spectral/rulesets/oas/index.json`, + ); + }); }); diff --git a/src/rulesets/oas/__tests__/message.spec.ts b/src/rulesets/__tests__/message.spec.ts similarity index 71% rename from src/rulesets/oas/__tests__/message.spec.ts rename to src/rulesets/__tests__/message.spec.ts index c235ea775..0b5f4c079 100644 --- a/src/rulesets/oas/__tests__/message.spec.ts +++ b/src/rulesets/__tests__/message.spec.ts @@ -1,4 +1,4 @@ -import { message } from '../../message'; +import { message } from '../message'; describe('message util', () => { test('interpolates correctly', () => { @@ -13,6 +13,18 @@ describe('message util', () => { ).toEqual('oops... "description" is missing;error: expected property to be truthy'); }); + test.each([0, false, null, undefined])('interpolates %s value correctly', value => { + const template = 'Value must not equal {{value}}'; + expect( + message(template, { + property: 'description', + error: 'expected property to be truthy', + path: '', + value, + }), + ).toEqual(`Value must not equal ${value}`); + }); + test('handles siblings', () => { const template = '{{error}}{{error}}{{property}}{{bar}}{{error}}{{error}}'; expect( diff --git a/src/rulesets/__tests__/reader.jest.test.ts b/src/rulesets/__tests__/reader.jest.test.ts index ebb724e7d..fae9dc292 100644 --- a/src/rulesets/__tests__/reader.jest.test.ts +++ b/src/rulesets/__tests__/reader.jest.test.ts @@ -3,21 +3,24 @@ import { Dictionary } from '@stoplight/types'; import { DiagnosticSeverity } from '@stoplight/types'; import * as fs from 'fs'; import * as nock from 'nock'; -import { IRule, Rule } from '../../types'; +import { Spectral } from '../../spectral'; +import { IRule, RuleType } from '../../types'; import { readRuleset } from '../reader'; -const nanoid = require('nanoid'); +const nanoid = require('nanoid/non-secure'); -jest.mock('nanoid'); +jest.mock('nanoid/non-secure'); +jest.mock('fs'); const validFlatRuleset = path.join(__dirname, './__fixtures__/valid-flat-ruleset.json'); const validRequireInfo = path.join(__dirname, './__fixtures__/valid-require-info-ruleset.yaml'); const github447 = path.join(__dirname, './__fixtures__/github-issue-447-fixture.yaml'); const enabledAllRuleset = path.join(__dirname, './__fixtures__/enable-all-ruleset.json'); const invalidRuleset = path.join(__dirname, './__fixtures__/invalid-ruleset.json'); -const extendsAllOas2Ruleset = path.join(__dirname, './__fixtures__/extends-oas2-ruleset.json'); -const extendsUnspecifiedOas2Ruleset = path.join(__dirname, './__fixtures__/extends-unspecified-oas2-ruleset.json'); -const extendsDisabledOas2Ruleset = path.join(__dirname, './__fixtures__/extends-disabled-oas2-ruleset.yaml'); -const extendsOas2WithOverrideRuleset = path.join(__dirname, './__fixtures__/extends-oas2-with-override-ruleset.json'); +const extendsOnlyRuleset = path.join(__dirname, './__fixtures__/extends-only-ruleset.json'); +const extendsAllOasRuleset = path.join(__dirname, './__fixtures__/extends-oas-ruleset.json'); +const extendsUnspecifiedOasRuleset = path.join(__dirname, './__fixtures__/extends-unspecified-oas-ruleset.json'); +const extendsDisabledOasRuleset = path.join(__dirname, './__fixtures__/extends-disabled-oas-ruleset.yaml'); +const extendsOasWithOverrideRuleset = path.join(__dirname, './__fixtures__/extends-oas-with-override-ruleset.json'); const extendsRelativeRuleset = path.join(__dirname, './__fixtures__/extends-relative-ruleset.json'); const myOpenAPIRuleset = path.join(__dirname, './__fixtures__/my-open-api-ruleset.json'); const fooRuleset = path.join(__dirname, './__fixtures__/foo-ruleset.json'); @@ -30,8 +33,7 @@ const fooCJSFunction = fs.readFileSync(path.join(__dirname, './__fixtures__/func const barFunction = fs.readFileSync(path.join(__dirname, './__fixtures__/customFunctions/bar.js'), 'utf8'); const truthyFunction = fs.readFileSync(path.join(__dirname, './__fixtures__/customFunctions/truthy.js'), 'utf8'); const oasRuleset = require('../oas/index.json'); -const oas2Ruleset = require('../oas2/index.json'); -const oas3Ruleset = require('../oas3/index.json'); +const oasRulesetRules: Dictionary = oasRuleset.rules; jest.setTimeout(10000); @@ -43,6 +45,7 @@ describe('Rulesets reader', () => { afterEach(() => { nock.cleanAll(); + nock.enableNetConnect(); }); it('given flat, valid ruleset file should return rules', async () => { @@ -52,7 +55,7 @@ describe('Rulesets reader', () => { 'valid-rule': { given: '$.info', message: 'should be OK', - severity: -1, + severity: DiagnosticSeverity.Warning, then: expect.any(Object), }, 'valid-rule-recommended': { @@ -74,7 +77,7 @@ describe('Rulesets reader', () => { 'valid-rule': { given: '$.info', message: 'should be OK', - severity: -1, + severity: DiagnosticSeverity.Warning, then: expect.any(Object), }, 'valid-rule-recommended': { @@ -87,7 +90,7 @@ describe('Rulesets reader', () => { 'require-info': { given: '$.info', message: 'should be OK', - severity: -1, + severity: DiagnosticSeverity.Warning, then: expect.any(Object), }, }, @@ -95,30 +98,60 @@ describe('Rulesets reader', () => { ); }); + it('given ruleset with no custom rules extending other rulesets', async () => { + const { rules } = await readRuleset(extendsOnlyRuleset); + + expect(rules).toEqual({ + 'bar-rule': { + given: '$.info', + message: 'should be OK', + recommended: true, + severity: DiagnosticSeverity.Warning, + then: { + function: expect.stringMatching(/^random-id-\d$/), + }, + }, + 'foo-rule': { + given: '$.info', + message: 'should be OK', + severity: DiagnosticSeverity.Warning, + then: { + function: expect.stringMatching(/^random-id-\d$/), + }, + }, + 'truthy-rule': { + given: '$.x', + message: 'should be OK', + recommended: true, + severity: DiagnosticSeverity.Warning, + then: { + function: expect.stringMatching(/^random-id-\d$/), + }, + }, + }); + }); + it('should inherit properties of extended rulesets', async () => { - const { rules } = await readRuleset(extendsAllOas2Ruleset); + const { rules } = await readRuleset(extendsAllOasRuleset); // we pick up *all* rules only from spectral:oas and spectral:oas2 and keep their severity level or set a default one expect(rules).toEqual( expect.objectContaining({ - ...[...Object.entries(oasRuleset.rules), ...Object.entries(oas2Ruleset.rules)].reduce>( - (oasRules, [name, rule]) => { - oasRules[name] = { - ...rule, - formats: expect.arrayContaining([expect.any(String)]), - ...((rule as IRule).severity === void 0 && { severity: DiagnosticSeverity.Warning }), - then: expect.any(Object), - }; + ...Object.entries(oasRulesetRules).reduce>((oasRules, [name, rule]) => { + oasRules[name] = { + ...rule, + formats: expect.arrayContaining([expect.any(String)]), + ...((rule as IRule).severity === void 0 && { severity: DiagnosticSeverity.Warning }), + then: expect.any(Object), + }; - return oasRules; - }, - {}, - ), + return oasRules; + }, {}), 'valid-rule': { given: '$.info', message: 'should be OK', - severity: -1, + severity: DiagnosticSeverity.Warning, then: expect.any(Object), }, }), @@ -126,28 +159,25 @@ describe('Rulesets reader', () => { }); it('should inherit properties of extended rulesets and disable not recommended ones', () => { - return expect(readRuleset(extendsUnspecifiedOas2Ruleset)).resolves.toEqual( + return expect(readRuleset(extendsUnspecifiedOasRuleset)).resolves.toEqual( expect.objectContaining({ rules: expect.objectContaining({ - ...[...Object.entries(oasRuleset.rules), ...Object.entries(oas2Ruleset.rules)].reduce>( - (rules, [name, rule]) => { - rules[name] = { - ...rule, - formats: expect.arrayContaining([expect.any(String)]), - ...((rule as IRule).severity === undefined && { severity: DiagnosticSeverity.Warning }), - ...(!(rule as IRule).recommended && { severity: -1 }), - then: expect.any(Object), - }; - - return rules; - }, - {}, - ), + ...Object.entries(oasRulesetRules).reduce>((rules, [name, rule]) => { + rules[name] = { + ...rule, + formats: expect.arrayContaining([expect.any(String)]), + ...((rule as IRule).severity === undefined && { severity: DiagnosticSeverity.Warning }), + ...((rule as IRule).recommended === false && { severity: -1 }), + then: expect.any(Object), + }; + + return rules; + }, {}), 'valid-rule': { given: '$.info', message: 'should be OK', - severity: -1, + severity: DiagnosticSeverity.Warning, then: expect.any(Object), }, }), @@ -170,13 +200,13 @@ describe('Rulesets reader', () => { expect(readRules).toEqual( expect.objectContaining({ - ...[...Object.entries(oasRuleset.rules), ...Object.entries(oas3Ruleset.rules)].reduce>( + ...Object.entries(oasRulesetRules).reduce>( (rules, [name, rule]) => { - const formattedRule: Rule = { - ...(rule as Rule), + const formattedRule: IRule = { + ...rule, formats: expect.arrayContaining([expect.any(String)]), - ...((rule as IRule).severity === undefined && { severity: DiagnosticSeverity.Warning }), - ...(!(rule as IRule).recommended && { severity: -1 }), + ...((rule as IRule).severity === void 0 && { severity: DiagnosticSeverity.Warning }), + ...((rule as IRule).recommended === false && { severity: -1 }), then: expect.any(Object), }; @@ -205,7 +235,7 @@ describe('Rulesets reader', () => { match: '^[A-Z][a-zA-Z0-9]*$', }, }, - type: 'style', + type: RuleType.STYLE, }, 'operation-id-kebab-case': { description: 'operationId MUST be written in kebab-case', @@ -221,7 +251,7 @@ describe('Rulesets reader', () => { match: '^[a-z][a-z0-9\\-]*$', }, }, - type: 'style', + type: RuleType.STYLE, }, }, ), @@ -230,20 +260,17 @@ describe('Rulesets reader', () => { }); it('should set severity of disabled rules to off', () => { - return expect(readRuleset(extendsDisabledOas2Ruleset)).resolves.toHaveProperty( + return expect(readRuleset(extendsDisabledOasRuleset)).resolves.toHaveProperty( 'rules', expect.objectContaining({ - ...[...Object.entries(oasRuleset.rules), ...Object.entries(oas2Ruleset.rules)].reduce>( - (rules, [name, rule]) => { - rules[name] = expect.objectContaining({ - description: (rule as IRule).description, - severity: -1, - }); + ...Object.entries(oasRuleset.rules).reduce>((rules, [name, rule]) => { + rules[name] = expect.objectContaining({ + description: (rule as IRule).description, + severity: -1, + }); - return rules; - }, - {}, - ), + return rules; + }, {}), 'operation-operationId-unique': expect.objectContaining({ // value of oasRuleset.rules['operation-operationId-unique'] @@ -259,7 +286,7 @@ describe('Rulesets reader', () => { }); it('should override properties of extended rulesets', () => { - return expect(readRuleset(extendsOas2WithOverrideRuleset)).resolves.toHaveProperty('rules.operation-2xx-response', { + return expect(readRuleset(extendsOasWithOverrideRuleset)).resolves.toHaveProperty('rules.operation-2xx-response', { description: 'should be overridden', given: '$.info', formats: expect.arrayContaining([expect.any(String)]), @@ -272,7 +299,7 @@ describe('Rulesets reader', () => { }); it('should persist disabled properties of extended rulesets', () => { - return expect(readRuleset(extendsOas2WithOverrideRuleset)).resolves.toHaveProperty( + return expect(readRuleset(extendsOasWithOverrideRuleset)).resolves.toHaveProperty( 'rules.oas2-operation-security-defined', { given: '$', @@ -291,17 +318,14 @@ describe('Rulesets reader', () => { const { rules: enabledRules } = await readRuleset(enabledAllRuleset); expect(enabledRules).toEqual( expect.objectContaining( - [...Object.entries(oasRuleset.rules), ...Object.entries(oas2Ruleset.rules)].reduce>( - (rules, [name, rule]) => { - rules[name] = expect.objectContaining({ - description: (rule as IRule).description, - ...((rule as IRule).severity === undefined && { severity: DiagnosticSeverity.Warning }), - }); - - return rules; - }, - {}, - ), + Object.entries(oasRuleset.rules).reduce>((rules, [name, rule]) => { + rules[name] = expect.objectContaining({ + description: (rule as IRule).description, + ...((rule as IRule).severity === undefined && { severity: DiagnosticSeverity.Warning }), + }); + + return rules; + }, {}), ), ); @@ -313,40 +337,16 @@ describe('Rulesets reader', () => { ).toHaveLength(0); }); - it('should limit the scope of formats to a ruleset', () => { - return expect(readRuleset(myOpenAPIRuleset)).resolves.toEqual( - expect.objectContaining({ - rules: { - ...Object.entries(oasRuleset.rules).reduce>((rules, [name, rule]) => { - rules[name] = expect.objectContaining({ - formats: ['oas2', 'oas3'], - }); - - return rules; - }, {}), + it('should limit the scope of formats to a ruleset', async () => { + const rules = (await readRuleset(myOpenAPIRuleset)).rules; - ...Object.entries(oas2Ruleset.rules).reduce>((rules, [name, rule]) => { - rules[name] = expect.objectContaining({ - formats: ['oas2'], - }); + expect(Object.keys(rules)).toHaveLength(4); - return rules; - }, {}), + expect(rules['my-valid-rule'].formats).toBeUndefined(); - ...Object.entries(oas3Ruleset.rules).reduce>((rules, [name, rule]) => { - rules[name] = expect.objectContaining({ - formats: ['oas3'], - }); - - return rules; - }, {}), - - 'valid-rule': expect.objectContaining({ - message: 'should be OK', - }), - }, - }), - ); + expect(rules['generic-valid-rule'].formats).toEqual(['oas2', 'oas3']); + expect(rules['oas2-valid-rule'].formats).toEqual(['oas2']); + expect(rules['oas3-valid-rule'].formats).toEqual(['oas3']); }); it('given spectral:oas ruleset, should not pick up unrecommended rules', () => { @@ -362,6 +362,159 @@ describe('Rulesets reader', () => { ); }); + it('given ruleset with extends set to all, should enable all rules', () => { + return expect( + readRuleset(path.join(__dirname, './__fixtures__/inheritanceRulesets/my-ruleset.json')), + ).resolves.toStrictEqual({ + functions: {}, + rules: { + 'contact-name-matches-stoplight': { + given: '$.info.contact', + message: 'Contact name must contain Stoplight', + recommended: false, + severity: DiagnosticSeverity.Warning, + then: { + field: 'name', + function: 'pattern', + functionOptions: { + match: 'Stoplight', + }, + }, + type: 'style', + }, + 'description-matches-stoplight': { + given: '$.info', + message: 'Description must contain Stoplight', + severity: DiagnosticSeverity.Error, + recommended: true, + then: { + field: 'description', + function: 'pattern', + functionOptions: { + match: 'Stoplight', + }, + }, + type: 'style', + }, + 'title-matches-stoplight': { + given: '$.info', + message: 'Title must contain Stoplight', + severity: DiagnosticSeverity.Warning, + then: { + field: 'title', + function: 'pattern', + functionOptions: { + match: 'Stoplight', + }, + }, + type: 'style', + }, + }, + }); + }); + + it('given ruleset with extends set to recommended, should enable recommended rules', () => { + return expect( + readRuleset(path.join(__dirname, './__fixtures__/inheritanceRulesets/my-ruleset-recommended.json')), + ).resolves.toStrictEqual({ + functions: {}, + rules: { + 'contact-name-matches-stoplight': { + given: '$.info.contact', + message: 'Contact name must contain Stoplight', + recommended: false, + severity: -1, + then: { + field: 'name', + function: 'pattern', + functionOptions: { + match: 'Stoplight', + }, + }, + type: 'style', + }, + 'description-matches-stoplight': { + given: '$.info', + message: 'Description must contain Stoplight', + severity: DiagnosticSeverity.Error, + recommended: true, + then: { + field: 'description', + function: 'pattern', + functionOptions: { + match: 'Stoplight', + }, + }, + type: 'style', + }, + 'title-matches-stoplight': { + given: '$.info', + message: 'Title must contain Stoplight', + severity: DiagnosticSeverity.Warning, + then: { + field: 'title', + function: 'pattern', + functionOptions: { + match: 'Stoplight', + }, + }, + type: 'style', + }, + }, + }); + }); + + it('given ruleset with extends set to off, should disable all rules', () => { + return expect( + readRuleset(path.join(__dirname, './__fixtures__/inheritanceRulesets/ruleset-c.json')), + ).resolves.toStrictEqual({ + functions: {}, + rules: { + 'contact-name-matches-stoplight': { + given: '$.info.contact', + message: 'Contact name must contain Stoplight', + recommended: false, + severity: -1, + then: { + field: 'name', + function: 'pattern', + functionOptions: { + match: 'Stoplight', + }, + }, + type: 'style', + }, + 'description-matches-stoplight': { + given: '$.info', + message: 'Description must contain Stoplight', + severity: -1, + recommended: true, + then: { + field: 'description', + function: 'pattern', + functionOptions: { + match: 'Stoplight', + }, + }, + type: 'style', + }, + 'title-matches-stoplight': { + given: '$.info', + message: 'Title must contain Stoplight', + severity: -1, + then: { + field: 'title', + function: 'pattern', + functionOptions: { + match: 'Stoplight', + }, + }, + type: 'style', + }, + }, + }); + }); + it('should support local rulesets', () => { return expect(readRuleset(extendsRelativeRuleset)).resolves.toEqual( expect.objectContaining({ @@ -369,7 +522,7 @@ describe('Rulesets reader', () => { PascalCase: { given: '$', message: 'bar', - severity: -1, // turned off, cause it's not recommended + severity: DiagnosticSeverity.Warning, then: { function: 'truthy', }, @@ -386,7 +539,7 @@ describe('Rulesets reader', () => { snake_case: { given: '$', message: 'foo', - severity: -1, + severity: DiagnosticSeverity.Warning, then: { function: 'truthy', }, @@ -415,7 +568,7 @@ describe('Rulesets reader', () => { 'foo-rule': expect.objectContaining({ message: 'should be OK', given: '$.info', - severity: -1, + severity: DiagnosticSeverity.Warning, then: { function: 'random-id-0', }, @@ -502,7 +655,7 @@ describe('Rulesets reader', () => { 'bar-rule': { given: '$.bar', message: 'Bar is truthy', - severity: -1, // rule was not recommended, hence the severity is set to false + severity: DiagnosticSeverity.Warning, then: { function: 'truthy', }, @@ -510,7 +663,7 @@ describe('Rulesets reader', () => { 'foo-rule': { given: '$.foo', message: 'Foo is falsy', - severity: -1, // rule was not recommended, hence the severity is set to false + severity: DiagnosticSeverity.Warning, then: { function: 'falsy', }, @@ -560,4 +713,41 @@ describe('Rulesets reader', () => { it('given invalid ruleset should output errors', () => { return expect(readRuleset(invalidRuleset)).rejects.toThrowError(/should have required property/); }); + + it('is able to load the whole ruleset from static file', async () => { + nock.disableNetConnect(); + + const readFileSpy = jest.spyOn(fs, 'readFile'); + + Spectral.registerStaticAssets(require('../../../rulesets/assets/assets.json')); + + const { rules, functions } = await readRuleset('spectral:oas'); + + expect(rules).toMatchObject({ + 'openapi-tags': expect.objectContaining({ + description: 'OpenAPI object should have non-empty `tags` array.', + formats: ['oas2', 'oas3'], + }), + 'oas2-schema': expect.objectContaining({ + description: 'Validate structure of OpenAPI v2 specification.', + formats: ['oas2'], + }), + 'oas3-schema': expect.objectContaining({ + description: 'Validate structure of OpenAPI v3 specification.', + formats: ['oas3'], + }), + }); + + expect(functions).toMatchObject({ + oasOp2xxResponse: expect.any(Object), + oasOpFormDataConsumeCheck: expect.any(Object), + oasOpIdUnique: expect.any(Object), + oasOpParams: expect.any(Object), + oasOpSecurityDefined: expect.any(Object), + oasPathParam: expect.any(Object), + }); + + expect(readFileSpy).not.toBeCalled(); + readFileSpy.mockRestore(); + }); }); diff --git a/src/rulesets/__tests__/reader.karma.test.ts b/src/rulesets/__tests__/reader.karma.test.ts new file mode 100644 index 000000000..3f1839e40 --- /dev/null +++ b/src/rulesets/__tests__/reader.karma.test.ts @@ -0,0 +1,47 @@ +import { FetchMockSandbox } from 'fetch-mock'; +import { Spectral } from '../../spectral'; +import { readRuleset } from '../reader'; + +declare const fetch: FetchMockSandbox; + +describe('Rulesets reader', () => { + afterEach(() => { + Spectral.registerStaticAssets({}); + }); + + it('is able to load the whole ruleset from static file', async () => { + fetch.resetBehavior(); + fetch.get('https://unpkg.com/@stoplight/spectral/rulesets/oas/index.json', { + status: 404, + body: {}, + }); + + Spectral.registerStaticAssets(require('../../../rulesets/assets/assets.json')); + + const { rules, functions } = await readRuleset('spectral:oas'); + + expect(rules).toMatchObject({ + 'openapi-tags': expect.objectContaining({ + description: 'OpenAPI object should have non-empty `tags` array.', + formats: ['oas2', 'oas3'], + }), + 'oas2-schema': expect.objectContaining({ + description: 'Validate structure of OpenAPI v2 specification.', + formats: ['oas2'], + }), + 'oas3-schema': expect.objectContaining({ + description: 'Validate structure of OpenAPI v3 specification.', + formats: ['oas3'], + }), + }); + + expect(functions).toMatchObject({ + oasOp2xxResponse: expect.any(Object), + oasOpFormDataConsumeCheck: expect.any(Object), + oasOpIdUnique: expect.any(Object), + oasOpParams: expect.any(Object), + oasOpSecurityDefined: expect.any(Object), + oasPathParam: expect.any(Object), + }); + }); +}); diff --git a/src/rulesets/__tests__/reader.test.ts b/src/rulesets/__tests__/reader.test.ts index 657291219..b1c23b8d8 100644 --- a/src/rulesets/__tests__/reader.test.ts +++ b/src/rulesets/__tests__/reader.test.ts @@ -2,7 +2,7 @@ import { readRuleset } from '../reader'; describe('Rulesets reader', () => { it('should resolve oas2-schema', async () => { - const { rules } = await readRuleset('spectral:oas2'); + const { rules } = await readRuleset('spectral:oas'); expect(rules['oas2-schema']).not.toHaveProperty('then.functionOptions.schema.$ref'); expect(rules['oas2-schema']).toHaveProperty( 'then.functionOptions.schema', @@ -15,7 +15,7 @@ describe('Rulesets reader', () => { }); it('should resolve oas3-schema', async () => { - const { rules } = await readRuleset('spectral:oas3'); + const { rules } = await readRuleset('spectral:oas'); expect(rules['oas3-schema']).not.toHaveProperty('then.functionOptions.schema.$ref'); expect(rules['oas3-schema']).toHaveProperty( 'then.functionOptions.schema', diff --git a/src/rulesets/__tests__/shared/_parameter-description.ts b/src/rulesets/__tests__/shared/_parameter-description.ts index 2b0170e7c..a61b5da37 100644 --- a/src/rulesets/__tests__/shared/_parameter-description.ts +++ b/src/rulesets/__tests__/shared/_parameter-description.ts @@ -123,6 +123,6 @@ export default (s: Spectral, oasVersion: number) => { }, }, }), - ).not.rejects; + ).resolves.toBeInstanceOf(Array); }); }; diff --git a/src/rulesets/__tests__/validation.test.ts b/src/rulesets/__tests__/validation.test.ts index f38ed07a9..ac78e8dea 100644 --- a/src/rulesets/__tests__/validation.test.ts +++ b/src/rulesets/__tests__/validation.test.ts @@ -10,9 +10,17 @@ describe('Ruleset Validation', () => { expect(assertValidRuleset.bind(null, 'true')).toThrow('Provided ruleset is not an object'); }); - it('given object with no rules property should throw', () => { - expect(assertValidRuleset.bind(null, {})).toThrow('Ruleset must have rules property'); - expect(assertValidRuleset.bind(null, { rule: {} })).toThrow('Ruleset must have rules property'); + it('given object with no rules and no extends properties should throw', () => { + expect(assertValidRuleset.bind(null, {})).toThrow('Ruleset must have rules or extends property'); + expect(assertValidRuleset.bind(null, { rule: {} })).toThrow('Ruleset must have rules or extends property'); + }); + + it('given object with extends property only should emit no errors', () => { + expect(assertValidRuleset.bind(null, { extends: [] })).not.toThrow(); + }); + + it('given object with rules property only should emit no errors', () => { + expect(assertValidRuleset.bind(null, { rules: {} })).not.toThrow(); }); it('given invalid ruleset should throw', () => { @@ -181,7 +189,10 @@ describe('Ruleset Validation', () => { it('recognizes valid array of functions with object only', () => { expect( assertValidRuleset.bind(null, { - functions: [['foo', {}], ['baz', {}]], + functions: [ + ['foo', {}], + ['baz', {}], + ], rules: {}, }), ).not.toThrow(); @@ -237,7 +248,7 @@ describe('Function Validation', () => { it('throws if options supplied to fn does not meet schema', () => { const schema: JSONSchema7 = { type: 'string' }; const wrapped = decorateIFunctionWithSchemaValidation(jest.fn(), schema); - expect(() => wrapped({}, 2, { given: [] }, { original: [], given: [] })).toThrow(ValidationError); + expect(() => wrapped({}, 2, { given: [] }, { original: [], given: [] } as any)).toThrow(ValidationError); }); it('does not call supplied fn if options do not meet schema', () => { @@ -245,20 +256,20 @@ describe('Function Validation', () => { const fn = jest.fn(); const wrapped = decorateIFunctionWithSchemaValidation(fn, schema); try { - wrapped({}, 2, { given: [] }, { original: [], given: [] }); + wrapped({}, 2, { given: [] }, { original: [], given: [] } as any); } catch { // will throw } expect(fn).not.toHaveBeenCalled(); - expect(() => wrapped({}, {}, { given: [] }, { original: [], given: [] })).toThrow(ValidationError); + expect(() => wrapped({}, {}, { given: [] }, { original: [], given: [] } as any)).toThrow(ValidationError); }); it('calls supplied fn and passes all other arguments if options do match schema', () => { const schema: JSONSchema7 = { type: 'string' }; const fn = jest.fn(); const wrapped = decorateIFunctionWithSchemaValidation(fn, schema); - wrapped({}, '2', { given: [] }, { original: [], given: [] }); + wrapped({}, '2', { given: [] }, { original: [], given: [] } as any); expect(fn).toHaveBeenCalledWith({}, '2', { given: [] }, { original: [], given: [] }); }); diff --git a/src/rulesets/evaluators.ts b/src/rulesets/evaluators.ts index 5b395b5d8..99bc4414f 100644 --- a/src/rulesets/evaluators.ts +++ b/src/rulesets/evaluators.ts @@ -1,6 +1,5 @@ import { Optional } from '@stoplight/types'; -import { JSONSchema7 } from 'json-schema'; -import { IFunction } from '../types'; +import { IFunction, JSONSchema } from '../types'; import { isObject } from '../utils/isObject'; import { decorateIFunctionWithSchemaValidation } from './validation'; @@ -62,7 +61,7 @@ export const evaluateExport = (body: string): Function => { return maybeFn; }; -export const compileExportedFunction = (code: string, name: string, schema: JSONSchema7 | null) => { +export const compileExportedFunction = (code: string, name: string, schema: JSONSchema | null) => { const exportedFn = evaluateExport(code) as IFunction; const fn = schema !== null ? decorateIFunctionWithSchemaValidation(exportedFn, schema) : exportedFn; diff --git a/src/rulesets/finder.ts b/src/rulesets/finder.ts index c15c7c5c6..31cee83ac 100644 --- a/src/rulesets/finder.ts +++ b/src/rulesets/finder.ts @@ -1,9 +1,8 @@ import * as path from '@stoplight/path'; import * as fs from 'fs'; -import { filesMap } from './map'; +import { RESOLVE_ALIASES, STATIC_ASSETS } from '../assets'; const SPECTRAL_SRC_ROOT = path.join(__dirname, '..'); - // DON'T RENAME THIS FUNCTION, you can move it within this file, but it must be kept as top-level declaration // parameter can be renamed, but don't this if you don't need to function resolveSpectralVersion(pkg: string) { @@ -32,6 +31,12 @@ async function resolveFromFS(from: string, to: string) { } targetPath = path.resolve(from, to); + + // if found in static assets, it's fine, as readParsable will handle it just fine + if (targetPath in STATIC_ASSETS) { + return targetPath; + } + // if it's not a built-in ruleset, try to resolve the file according to the provided path if (await exists(targetPath)) { return targetPath; @@ -41,11 +46,16 @@ async function resolveFromFS(from: string, to: string) { } export async function findFile(from: string, to: string) { - const mapped = filesMap.get(to); + const mapped = RESOLVE_ALIASES[to]; + if (mapped !== void 0) { to = mapped; } + if (to in STATIC_ASSETS) { + return to; + } + if (path.isAbsolute(to)) { return to; } diff --git a/src/rulesets/map.ts b/src/rulesets/map.ts deleted file mode 100644 index fe8f980ba..000000000 --- a/src/rulesets/map.ts +++ /dev/null @@ -1,9 +0,0 @@ -function resolveSpectralRuleset(ruleset: string) { - return `@stoplight/spectral/rulesets/${ruleset}/index.json`; -} - -export const filesMap = new Map([ - ['spectral:oas', resolveSpectralRuleset('oas')], - ['spectral:oas2', resolveSpectralRuleset('oas2')], - ['spectral:oas3', resolveSpectralRuleset('oas3')], -]); diff --git a/src/rulesets/mergers/__tests__/functions.jest.test.ts b/src/rulesets/mergers/__tests__/functions.jest.test.ts index 2fbd364d0..8c73da171 100644 --- a/src/rulesets/mergers/__tests__/functions.jest.test.ts +++ b/src/rulesets/mergers/__tests__/functions.jest.test.ts @@ -1,9 +1,9 @@ import { RuleCollection } from '../../../types'; import { RulesetFunctionCollection } from '../../../types/ruleset'; import { mergeFunctions } from '../functions'; -const nanoid = require('nanoid'); +const nanoid = require('nanoid/non-secure'); -jest.mock('nanoid'); +jest.mock('nanoid/non-secure'); describe('Ruleset functions merging', () => { beforeEach(() => { diff --git a/src/rulesets/mergers/__tests__/rules.jest.test.ts b/src/rulesets/mergers/__tests__/rules.jest.test.ts index 187113da3..f30098094 100644 --- a/src/rulesets/mergers/__tests__/rules.jest.test.ts +++ b/src/rulesets/mergers/__tests__/rules.jest.test.ts @@ -1,5 +1,6 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { IRule } from '../../../types'; +import { FileRuleCollection } from '../../../types/ruleset'; import { mergeRules } from '../rules'; describe('Ruleset rules merging', () => { @@ -146,18 +147,6 @@ describe('Ruleset rules merging', () => { expect(rules).toHaveProperty('test.severity', -1); }); - it('supports array-ish syntax', () => { - const rules = { - test: JSON.parse(JSON.stringify(baseRule)), - }; - - mergeRules(rules, { - test: ['off'], - }); - - expect(rules).toHaveProperty('test.severity', -1); - }); - it('does not set functionOptions if rule does not implement it', () => { const rules = { test: JSON.parse(JSON.stringify(baseRule)), @@ -203,6 +192,9 @@ describe('Ruleset rules merging', () => { it('picks up recommended rules', () => { const rules = {}; + const test3 = JSON.parse(JSON.stringify(baseRule)); + delete test3.recommended; + mergeRules( rules, { @@ -211,12 +203,49 @@ describe('Ruleset rules merging', () => { ...JSON.parse(JSON.stringify(baseRule)), recommended: false, }, + test3, }, 'recommended', ); expect(rules).toHaveProperty('test.severity', DiagnosticSeverity.Warning); expect(rules).toHaveProperty('test2.severity', -1); + expect(rules).toHaveProperty('test3.severity', DiagnosticSeverity.Warning); + }); + + it('rules with no severity and no recommended set are treated as warnings', () => { + const rules = {}; + + const rule = JSON.parse(JSON.stringify(baseRule)); + delete rule.recommended; + delete rule.severity; + + mergeRules( + rules, + { + rule, + }, + 'recommended', + ); + + expect(rules).toHaveProperty('rule.severity', DiagnosticSeverity.Warning); + }); + + it('rules with recommended set to false are disabled', () => { + const rules = {}; + + mergeRules( + rules, + { + rule: { + ...JSON.parse(JSON.stringify(baseRule)), + recommended: false, + }, + }, + 'recommended', + ); + + expect(rules).toHaveProperty('rule.severity', -1); }); it('sets warning as default severity level if a rule has no severity specified', () => { @@ -284,6 +313,121 @@ describe('Ruleset rules merging', () => { expect(rules).toHaveProperty('test.severity', DiagnosticSeverity.Hint); }); + describe('inheriting rules with no explicit severity levels', () => { + let rules: FileRuleCollection; + + beforeEach(() => { + rules = { + rule: { + given: '', + then: { function: '' }, + recommended: true, + }, + 'rule-with-no-recommended': { + given: '', + then: { function: '' }, + }, + 'optional-rule': { + given: '', + then: { function: '' }, + recommended: false, + }, + }; + }); + + it('sets warning as a default', () => { + const newRules = mergeRules({}, rules); + + const custom = { + rule: true, + 'rule-with-no-recommended': true, + 'optional-rule': true, + }; + + expect(mergeRules(newRules, custom)).toEqual({ + rule: expect.objectContaining({ + recommended: true, + severity: DiagnosticSeverity.Warning, + }), + 'rule-with-no-recommended': expect.objectContaining({ + severity: DiagnosticSeverity.Warning, + }), + 'optional-rule': expect.objectContaining({ + recommended: false, + severity: DiagnosticSeverity.Warning, + }), + }); + }); + + it('sets warning as a default even when all rules were disabled', () => { + const newRules = mergeRules({}, rules, 'off'); + + const custom = { + rule: true, + 'rule-with-no-recommended': true, + 'optional-rule': true, + }; + + expect(mergeRules(newRules, custom)).toEqual({ + rule: expect.objectContaining({ + recommended: true, + severity: DiagnosticSeverity.Warning, + }), + 'rule-with-no-recommended': expect.objectContaining({ + severity: DiagnosticSeverity.Warning, + }), + 'optional-rule': expect.objectContaining({ + recommended: false, + severity: DiagnosticSeverity.Warning, + }), + }); + }); + + it('respects ruleset severity', () => { + expect(mergeRules({}, rules, 'all')).toEqual({ + rule: expect.objectContaining({ + recommended: true, + severity: DiagnosticSeverity.Warning, + }), + 'rule-with-no-recommended': expect.objectContaining({ + severity: DiagnosticSeverity.Warning, + }), + 'optional-rule': expect.objectContaining({ + recommended: false, + severity: DiagnosticSeverity.Warning, + }), + }); + + expect(mergeRules({}, rules, 'recommended')).toEqual({ + rule: expect.objectContaining({ + recommended: true, + severity: DiagnosticSeverity.Warning, + }), + 'rule-with-no-recommended': expect.objectContaining({ + severity: DiagnosticSeverity.Warning, + }), + 'optional-rule': expect.objectContaining({ + recommended: false, + severity: -1, + }), + }); + + expect(mergeRules({}, rules, 'off')).toEqual({ + rule: expect.objectContaining({ + recommended: true, + severity: -1, + }), + 'rule-with-no-recommended': expect.objectContaining({ + severity: -1, + }), + 'optional-rule': expect.objectContaining({ + recommended: false, + severity: -1, + }), + }); + }); + }); + it('given invalid rule value should throw', () => { const rules = { test: JSON.parse(JSON.stringify(baseRule)), diff --git a/src/rulesets/mergers/functions.ts b/src/rulesets/mergers/functions.ts index 4637fa097..020a1a64f 100644 --- a/src/rulesets/mergers/functions.ts +++ b/src/rulesets/mergers/functions.ts @@ -1,7 +1,7 @@ import { Dictionary } from '@stoplight/types/dist'; import { RuleCollection } from '../../types'; import { RulesetFunctionCollection } from '../../types/ruleset'; -const nanoid = require('nanoid'); +const nanoid = require('nanoid/non-secure'); export function mergeFunctions( target: RulesetFunctionCollection, diff --git a/src/rulesets/mergers/rules.ts b/src/rulesets/mergers/rules.ts index 0ff0c37a7..3e4f152c7 100644 --- a/src/rulesets/mergers/rules.ts +++ b/src/rulesets/mergers/rules.ts @@ -53,7 +53,7 @@ function updateRootRule(root: Rule, newRule: Rule | null) { Object.assign(root[ROOT_DESCRIPTOR], copyRule(newRule === null ? root : Object.assign(root, newRule))); } -function getRootRule(rule: Rule): Rule { +function getRootRule(rule: Rule): Rule | null { return rule[ROOT_DESCRIPTOR] !== undefined ? rule[ROOT_DESCRIPTOR] : null; } @@ -68,11 +68,16 @@ function processRule(rules: FileRuleCollection, name: string, rule: FileRule | F case 'boolean': if (isValidRule(existingRule)) { const rootRule = getRootRule(existingRule); - if (rule) { - existingRule.severity = rootRule ? rootRule.severity : getSeverityLevel(rules, name, rule); + if (!rule) { + existingRule.severity = -1; + } else if (rootRule === null) { + existingRule.severity = getSeverityLevel(rules, name, rule); + updateRootRule(existingRule, existingRule); + } else if ('severity' in rootRule) { + existingRule.severity = rootRule.severity; updateRootRule(existingRule, existingRule); } else { - existingRule.severity = -1; + existingRule.severity = DiagnosticSeverity.Warning; } } break; @@ -113,7 +118,7 @@ function processRule(rules: FileRuleCollection, name: string, rule: FileRule | F function normalizeRule(rule: Rule, severity: DiagnosticSeverity | HumanReadableDiagnosticSeverity | undefined) { if (rule.severity === void 0) { - rule.severity = severity === void 0 ? (rule.recommended ? DEFAULT_SEVERITY_LEVEL : -1) : severity; + rule.severity = severity === void 0 ? (rule.recommended !== false ? DEFAULT_SEVERITY_LEVEL : -1) : severity; } else { rule.severity = getDiagnosticSeverity(rule.severity); } diff --git a/src/rulesets/message.ts b/src/rulesets/message.ts index c0f2967c8..c09375326 100644 --- a/src/rulesets/message.ts +++ b/src/rulesets/message.ts @@ -1,4 +1,6 @@ import { Segment } from '@stoplight/types'; +import { isObject } from 'lodash'; +import { Replacer } from '../utils/replacer'; export interface IMessageVars { property: Segment; @@ -10,18 +12,23 @@ export interface IMessageVars { export type MessageInterpolator = (str: string, values: IMessageVars) => string; -const BRACES = /{{([^}]+)}}/g; +const MessageReplacer = new Replacer(2); -export const message: MessageInterpolator = (str, values) => { - BRACES.lastIndex = 0; - let result: RegExpExecArray | null = null; +MessageReplacer.addTransformer('double-quotes', (id, value) => (value ? `"${value}"` : '')); +MessageReplacer.addTransformer('single-quotes', (id, value) => (value ? `'${value}'` : '')); +MessageReplacer.addTransformer('gravis', (id, value) => (value ? `\`${value}\`` : '')); - // tslint:disable-next-line:no-conditional-assignment - while ((result = BRACES.exec(str))) { - const newValue = String(values[result[1]] || ''); - str = `${str.slice(0, result.index)}${newValue}${str.slice(BRACES.lastIndex)}`; - BRACES.lastIndex = result.index + newValue.length; +MessageReplacer.addTransformer('append-property', (id, value) => (value ? `${value} property ` : '')); +MessageReplacer.addTransformer('optional-typeof', (id, value, values) => + value ? String(value) : `${typeof values.value} `, +); + +MessageReplacer.addTransformer('to-string', (id, value) => { + if (isObject(value)) { + return Array.isArray(value) ? 'Array[]' : 'Object{}'; } - return str; -}; + return JSON.stringify(value); +}); + +export const message: MessageInterpolator = MessageReplacer.print.bind(MessageReplacer); diff --git a/src/rulesets/oas/__tests__/contact-properties.ts b/src/rulesets/oas/__tests__/contact-properties.ts index bac9b051f..f458dd6cc 100644 --- a/src/rulesets/oas/__tests__/contact-properties.ts +++ b/src/rulesets/oas/__tests__/contact-properties.ts @@ -33,39 +33,6 @@ describe('contact-properties', () => { info: { contact: {} }, }); expect(results).toEqual([ - { - code: 'contact-properties', - message: 'Contact object should have `name`, `url` and `email`.', - path: ['info', 'contact'], - range: { - end: { - character: 17, - line: 4, - }, - start: { - character: 14, - line: 4, - }, - }, - severity: 1, - source: undefined, - }, - { - code: 'contact-properties', - message: 'Contact object should have `name`, `url` and `email`.', - path: ['info', 'contact'], - range: { - end: { - character: 17, - line: 4, - }, - start: { - character: 14, - line: 4, - }, - }, - severity: DiagnosticSeverity.Warning, - }, { code: 'contact-properties', message: 'Contact object should have `name`, `url` and `email`.', diff --git a/src/rulesets/oas/__tests__/example-value-or-externalValue.ts b/src/rulesets/oas/__tests__/example-value-or-externalValue.ts index ec5a03337..e53811705 100644 --- a/src/rulesets/oas/__tests__/example-value-or-externalValue.ts +++ b/src/rulesets/oas/__tests__/example-value-or-externalValue.ts @@ -21,7 +21,7 @@ describe('example-value-or-externalValue', () => { expect(results.length).toEqual(0); }); - test('return errors if missing externalValue and value', async () => { + test('return warnings if missing externalValue and value', async () => { const results = await s.run({ example: {} }); expect(results).toEqual([ { @@ -43,7 +43,7 @@ describe('example-value-or-externalValue', () => { ]); }); - test('return errors if both externalValue and value', async () => { + test('return warnings if both externalValue and value', async () => { const results = await s.run({ example: { externalValue: 'externalValue', value: 'value' } }); expect(results).toEqual([ { @@ -60,7 +60,7 @@ describe('example-value-or-externalValue', () => { line: 1, }, }, - severity: 1, + severity: DiagnosticSeverity.Warning, }, ]); }); diff --git a/src/rulesets/oas2/__tests__/oas2-anyOf.ts b/src/rulesets/oas/__tests__/oas2-anyOf.ts similarity index 97% rename from src/rulesets/oas2/__tests__/oas2-anyOf.ts rename to src/rulesets/oas/__tests__/oas2-anyOf.ts index ec3e5582a..d79ee4c65 100644 --- a/src/rulesets/oas2/__tests__/oas2-anyOf.ts +++ b/src/rulesets/oas/__tests__/oas2-anyOf.ts @@ -3,6 +3,7 @@ import * as ruleset from '../index.json'; describe('oas2-anyOf', () => { const s = new Spectral(); + s.registerFormat('oas2', () => true); s.setRules({ 'oas2-anyOf': Object.assign(ruleset.rules['oas2-anyOf'], { recommended: true, diff --git a/src/rulesets/oas2/__tests__/api-host.ts b/src/rulesets/oas/__tests__/oas2-api-host.ts similarity index 80% rename from src/rulesets/oas2/__tests__/api-host.ts rename to src/rulesets/oas/__tests__/oas2-api-host.ts index f515e3d69..90e3490d7 100644 --- a/src/rulesets/oas2/__tests__/api-host.ts +++ b/src/rulesets/oas/__tests__/oas2-api-host.ts @@ -2,12 +2,13 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { RuleType, Spectral } from '../../../spectral'; import * as ruleset from '../index.json'; -describe('api-host', () => { +describe('oas2-api-host', () => { const s = new Spectral(); + s.registerFormat('oas2', () => true); s.setRules({ - 'api-host': Object.assign(ruleset.rules['api-host'], { + 'oas2-api-host': Object.assign(ruleset.rules['oas2-api-host'], { recommended: true, - type: RuleType[ruleset.rules['api-host'].type], + type: RuleType[ruleset.rules['oas2-api-host'].type], }), }); @@ -28,7 +29,7 @@ describe('api-host', () => { expect(results).toEqual([ { - code: 'api-host', + code: 'oas2-api-host', message: 'OpenAPI `host` must be present and non-empty string.', path: [], range: { diff --git a/src/rulesets/oas2/__tests__/api-schemes.ts b/src/rulesets/oas/__tests__/oas2-api-schemes.ts similarity index 84% rename from src/rulesets/oas2/__tests__/api-schemes.ts rename to src/rulesets/oas/__tests__/oas2-api-schemes.ts index 9435c6684..79b735aed 100644 --- a/src/rulesets/oas2/__tests__/api-schemes.ts +++ b/src/rulesets/oas/__tests__/oas2-api-schemes.ts @@ -2,12 +2,13 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { RuleType, Spectral } from '../../../spectral'; import * as ruleset from '../index.json'; -describe('api-schemes', () => { +describe('oas2-api-schemes', () => { const s = new Spectral(); + s.registerFormat('oas2', () => true); s.setRules({ - 'api-schemes': Object.assign(ruleset.rules['api-schemes'], { + 'oas2-api-schemes': Object.assign(ruleset.rules['oas2-api-schemes'], { recommended: true, - type: RuleType[ruleset.rules['api-schemes'].type], + type: RuleType[ruleset.rules['oas2-api-schemes'].type], }), }); @@ -27,7 +28,7 @@ describe('api-schemes', () => { }); expect(results).toEqual([ { - code: 'api-schemes', + code: 'oas2-api-schemes', message: 'OpenAPI host `schemes` must be present and non-empty array.', path: [], range: { @@ -53,7 +54,7 @@ describe('api-schemes', () => { }); expect(results).toEqual([ { - code: 'api-schemes', + code: 'oas2-api-schemes', message: 'OpenAPI host `schemes` must be present and non-empty array.', path: ['schemes'], range: { diff --git a/src/rulesets/oas2/__tests__/host-not-example.ts b/src/rulesets/oas/__tests__/oas2-host-not-example.ts similarity index 66% rename from src/rulesets/oas2/__tests__/host-not-example.ts rename to src/rulesets/oas/__tests__/oas2-host-not-example.ts index b9bcfcc1f..e845ffb9f 100644 --- a/src/rulesets/oas2/__tests__/host-not-example.ts +++ b/src/rulesets/oas/__tests__/oas2-host-not-example.ts @@ -1,12 +1,13 @@ import { RuleType, Spectral } from '../../../spectral'; import * as ruleset from '../index.json'; -describe('host-not-example', () => { +describe('oas2-host-not-example', () => { const s = new Spectral(); + s.registerFormat('oas2', () => true); s.setRules({ - 'host-not-example': Object.assign(ruleset.rules['host-not-example'], { + 'oas2-host-not-example': Object.assign(ruleset.rules['oas2-host-not-example'], { recommended: true, - type: RuleType[ruleset.rules['host-not-example'].type], + type: RuleType[ruleset.rules['oas2-host-not-example'].type], }), }); @@ -27,8 +28,8 @@ describe('host-not-example', () => { }); expect(results).toEqual([ expect.objectContaining({ - code: 'host-not-example', - message: 'Server URL should not point at `example.com`.', + code: 'oas2-host-not-example', + message: 'Host URL should not point at example.com.', path: ['host'], }), ]); diff --git a/src/rulesets/oas2/__tests__/host-trailing-slash.ts b/src/rulesets/oas/__tests__/oas2-host-trailing-slash.ts similarity index 77% rename from src/rulesets/oas2/__tests__/host-trailing-slash.ts rename to src/rulesets/oas/__tests__/oas2-host-trailing-slash.ts index cffc548a8..7e988d78e 100644 --- a/src/rulesets/oas2/__tests__/host-trailing-slash.ts +++ b/src/rulesets/oas/__tests__/oas2-host-trailing-slash.ts @@ -2,12 +2,13 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { RuleType, Spectral } from '../../../spectral'; import * as ruleset from '../index.json'; -describe('host-trailing-slash', () => { +describe('oas2-host-trailing-slash', () => { const s = new Spectral(); + s.registerFormat('oas2', () => true); s.setRules({ - 'host-trailing-slash': Object.assign(ruleset.rules['host-trailing-slash'], { + 'oas2-host-trailing-slash': Object.assign(ruleset.rules['oas2-host-trailing-slash'], { recommended: true, - type: RuleType[ruleset.rules['host-trailing-slash'].type], + type: RuleType[ruleset.rules['oas2-host-trailing-slash'].type], }), }); @@ -28,7 +29,7 @@ describe('host-trailing-slash', () => { }); expect(results).toEqual([ { - code: 'host-trailing-slash', + code: 'oas2-host-trailing-slash', message: 'Server URL should not have a trailing slash.', path: ['host'], range: { diff --git a/src/rulesets/oas2/__tests__/oas2-oneOf.ts b/src/rulesets/oas/__tests__/oas2-oneOf.ts similarity index 90% rename from src/rulesets/oas2/__tests__/oas2-oneOf.ts rename to src/rulesets/oas/__tests__/oas2-oneOf.ts index c8e24ba94..cd9778a18 100644 --- a/src/rulesets/oas2/__tests__/oas2-oneOf.ts +++ b/src/rulesets/oas/__tests__/oas2-oneOf.ts @@ -3,6 +3,7 @@ import * as ruleset from '../index.json'; describe('oas2-oneOf', () => { const s = new Spectral(); + s.registerFormat('oas2', () => true); s.setRules({ 'oas2-oneOf': Object.assign(ruleset.rules['oas2-oneOf'], { recommended: true, @@ -25,7 +26,7 @@ describe('oas2-oneOf', () => { 200: { description: 'A paged array of pets', schema: { - oneOf: [{ type: 'string' }, { type: null }], + oneOf: [{ type: 'string' }, { type: 'null' }], }, }, }, @@ -40,7 +41,7 @@ describe('oas2-oneOf', () => { path: ['paths', '/test', 'get', 'responses', '200', 'schema', 'oneOf'], range: { end: { - character: 30, + character: 32, line: 21, }, start: { diff --git a/src/rulesets/oas2/__tests__/oas2-parameter-description.ts b/src/rulesets/oas/__tests__/oas2-parameter-description.ts similarity index 96% rename from src/rulesets/oas2/__tests__/oas2-parameter-description.ts rename to src/rulesets/oas/__tests__/oas2-parameter-description.ts index 1187c2e4a..f3745cc5c 100644 --- a/src/rulesets/oas2/__tests__/oas2-parameter-description.ts +++ b/src/rulesets/oas/__tests__/oas2-parameter-description.ts @@ -5,7 +5,7 @@ import * as ruleset from '../index.json'; describe('oas2-parameter-description', () => { const s = new Spectral(); - + s.registerFormat('oas2', () => true); s.setRules({ 'oas2-parameter-description': Object.assign(ruleset.rules['oas2-parameter-description'], { recommended: true, diff --git a/src/rulesets/oas2/__tests__/oas2-schema.ts b/src/rulesets/oas/__tests__/oas2-schema.ts similarity index 86% rename from src/rulesets/oas2/__tests__/oas2-schema.ts rename to src/rulesets/oas/__tests__/oas2-schema.ts index 6b5c8dfe5..12936ccb6 100644 --- a/src/rulesets/oas2/__tests__/oas2-schema.ts +++ b/src/rulesets/oas/__tests__/oas2-schema.ts @@ -1,10 +1,10 @@ import { RuleType, Spectral } from '../../../spectral'; import * as ruleset from '../index.json'; -import * as oas2Schema from '../schemas/main.json'; +import * as oas2Schema from '../schemas/schema.oas2.json'; describe('oas2-schema', () => { const s = new Spectral(); - + s.registerFormat('oas2', () => true); s.setRules({ 'oas2-schema': Object.assign({}, ruleset.rules['oas2-schema'], { recommended: true, @@ -35,7 +35,7 @@ describe('oas2-schema', () => { expect(results).toEqual([ { code: 'oas2-schema', - message: "/paths//test/get should have required property 'responses'", + message: `\`get\` property should have required property \`responses\``, path: ['paths', '/test', 'get'], range: { end: { diff --git a/src/rulesets/oas/__tests__/oas2-unused-definition.jest.test.ts b/src/rulesets/oas/__tests__/oas2-unused-definition.jest.test.ts new file mode 100644 index 000000000..0ec4e5d40 --- /dev/null +++ b/src/rulesets/oas/__tests__/oas2-unused-definition.jest.test.ts @@ -0,0 +1,168 @@ +import { getLocationForJsonPath, parseWithPointers } from '@stoplight/json'; +import * as path from '@stoplight/path'; +import { unreferencedReusableObject } from '../../../functions/unreferencedReusableObject'; +import { IParsedResult, RuleType, Spectral } from '../../../index'; +import { httpAndFileResolver } from '../../../resolvers/http-and-file'; +import { rules } from '../index.json'; + +import { DiagnosticSeverity } from '@stoplight/types'; +import * as nock from 'nock'; +import { readParsable } from '../../../fs/reader'; + +describe('unusedDefinition - Http and fs remote references', () => { + const s = new Spectral({ resolver: httpAndFileResolver }); + s.registerFormat('oas2', () => true); + s.setFunctions({ unreferencedReusableObject }); + s.setRules({ + 'oas2-unused-definition': Object.assign(rules['oas2-unused-definition'], { + recommended: true, + type: RuleType[rules['oas2-unused-definition'].type], + }), + }); + + describe('reports unreferenced definitions', () => { + test('when analyzing an in-memory document', async () => { + nock('https://oas2.library.com') + .get('/defs.json') + .reply( + 200, + JSON.stringify({ + definitions: { + ExternalHttp: { + type: 'number', + }, + }, + }), + ); + + const remoteFsRefeferencePath = path.join( + __dirname, + '../../__tests__/__fixtures__/unusedDefinition.definition.json#/definitions/ExternalFs', + ); + + const doc = `{ + "swagger": "2.0", + "x-hook": { + "$ref": "#/definitions/Hooked" + }, + "x-also-hook": { + "$ref": "#/definitions/Hooked" + }, + "paths": { + "/path": { + "post": { + "parameters": [ + { + "$ref": "#/definitions/HookedAsWell" + }, + { + "$ref": "${remoteFsRefeferencePath}" + }, + { + "$ref": "https://oas2.library.com/defs.json#/definitions/ExternalHttp" + } + ] + } + } + }, + "definitions": { + "Hooked": { + "type": "object" + }, + "HookedAsWell": { + "name": "value", + "in": "query", + "type": "number" + }, + "Unhooked": { + "type": "object" + } + } + }`; + + const results = await s.run({ + parsed: parseWithPointers(doc), + getLocationForJsonPath, + }); + + expect(results).toEqual([ + { + code: 'oas2-unused-definition', + message: 'Potentially unused definition has been detected.', + path: ['definitions', 'Unhooked'], + range: { + end: { + character: 9, + line: 36, + }, + start: { + character: 20, + line: 34, + }, + }, + severity: DiagnosticSeverity.Warning, + }, + ]); + + nock.cleanAll(); + }); + + test('when analyzing a directly self-referencing document from the filesystem', async () => { + const fixturePath = path.join(__dirname, '../../__tests__/__fixtures__/unusedDefinition.remoteLocal.json'); + + const spec = parseWithPointers(await readParsable(fixturePath, { encoding: 'utf8' })); + + const parsedResult: IParsedResult = { + source: fixturePath, + parsed: spec, + getLocationForJsonPath, + }; + + const results = await s.run(parsedResult, { + resolve: { + documentUri: fixturePath, + }, + }); + + expect(results).toEqual([]); + }); + + test('when analyzing an indirectly self-referencing document from the filesystem', async () => { + const fixturePath = path.join(__dirname, '../../__tests__/__fixtures__/unusedDefinition.indirect.1.json'); + + const spec = parseWithPointers(await readParsable(fixturePath, { encoding: 'utf8' })); + + const parsedResult: IParsedResult = { + source: fixturePath, + parsed: spec, + getLocationForJsonPath, + }; + + const results = await s.run(parsedResult, { + resolve: { + documentUri: fixturePath, + }, + }); + + expect(results).toEqual([ + { + code: 'oas2-unused-definition', + message: 'Potentially unused definition has been detected.', + path: ['definitions', 'Unhooked'], + range: { + end: { + character: 5, + line: 11, + }, + start: { + character: 16, + line: 9, + }, + }, + severity: DiagnosticSeverity.Warning, + source: expect.stringMatching('/__tests__/__fixtures__/unusedDefinition.indirect.1.json$'), + }, + ]); + }); + }); +}); diff --git a/src/rulesets/oas/__tests__/oas2-unused-definition.karma.test.ts b/src/rulesets/oas/__tests__/oas2-unused-definition.karma.test.ts new file mode 100644 index 000000000..c56dc1c5a --- /dev/null +++ b/src/rulesets/oas/__tests__/oas2-unused-definition.karma.test.ts @@ -0,0 +1,104 @@ +import { getLocationForJsonPath, parseWithPointers } from '@stoplight/json'; +import { DiagnosticSeverity } from '@stoplight/types'; +import { FetchMockSandbox } from 'fetch-mock'; +import { unreferencedReusableObject } from '../../../functions/unreferencedReusableObject'; +import { RuleType, Spectral } from '../../../index'; +import { httpAndFileResolver } from '../../../resolvers/http-and-file'; +import { rules } from '../index.json'; + +describe('unusedDefinition - Http remote references', () => { + let fetchMock: FetchMockSandbox; + + const s = new Spectral({ resolver: httpAndFileResolver }); + s.registerFormat('oas2', () => true); + s.setFunctions({ unreferencedReusableObject }); + s.setRules({ + 'oas2-unused-definition': Object.assign(rules['oas2-unused-definition'], { + recommended: true, + type: RuleType[rules['oas2-unused-definition'].type], + }), + }); + + beforeEach(() => { + fetchMock = require('fetch-mock').sandbox(); + window.fetch = fetchMock; + }); + + afterEach(() => { + window.fetch = fetch; + }); + + test('reports unreferenced definitions', async () => { + fetchMock.mock('https://oas2.library.com/defs.json', { + status: 200, + body: { + definitions: { + ExternalHttp: { + type: 'number', + }, + }, + }, + }); + + const doc = `{ + "swagger": "2.0", + "x-hook": { + "$ref": "#/definitions/Hooked" + }, + "x-also-hook": { + "$ref": "#/definitions/Hooked" + }, + "paths": { + "/path": { + "post": { + "parameters": [ + { + "$ref": "#/definitions/HookedAsWell" + }, + { + "$ref": "https://oas2.library.com/defs.json#/definitions/ExternalHttp" + } + ] + } + } + }, + "definitions": { + "Hooked": { + "type": "object" + }, + "HookedAsWell": { + "name": "value", + "in": "query", + "type": "number" + }, + "Unhooked": { + "type": "object" + } + } + }`; + + const results = await s.run({ + parsed: parseWithPointers(doc), + getLocationForJsonPath, + }); + + expect(results).toEqual([ + { + code: 'oas2-unused-definition', + message: 'Potentially unused definition has been detected.', + path: ['definitions', 'Unhooked'], + range: { + end: { + character: 9, + line: 33, + }, + start: { + character: 20, + line: 31, + }, + }, + severity: DiagnosticSeverity.Warning, + }, + ]); + }); +}); diff --git a/src/rulesets/oas/__tests__/oas2-unused-definition.test.ts b/src/rulesets/oas/__tests__/oas2-unused-definition.test.ts new file mode 100644 index 000000000..46c1c5266 --- /dev/null +++ b/src/rulesets/oas/__tests__/oas2-unused-definition.test.ts @@ -0,0 +1,147 @@ +import { getLocationForJsonPath, parseWithPointers } from '@stoplight/json'; +import { DiagnosticSeverity } from '@stoplight/types'; +import { unreferencedReusableObject } from '../../../functions/unreferencedReusableObject'; +import { RuleType, Spectral } from '../../../index'; +import { httpAndFileResolver } from '../../../resolvers/http-and-file'; +import { rules } from '../index.json'; + +describe('oas2-unused-definition - local references', () => { + const s = new Spectral({ resolver: httpAndFileResolver }); + s.registerFormat('oas2', () => true); + s.setFunctions({ unreferencedReusableObject }); + s.setRules({ + 'oas2-unused-definition': Object.assign(rules['oas2-unused-definition'], { + recommended: true, + type: RuleType[rules['oas2-unused-definition'].type], + }), + }); + + test('does not report anything for empty object', async () => { + const results = await s.run({ + swagger: '2.0', + }); + + expect(results).toEqual([]); + }); + + test('does not throw when meeting an invalid json pointer', async () => { + const doc = `{ + "swagger": "2.0", + "x-hook": { + "$ref": "'$#@!!!' What?" + }, + "paths": { + }, + "definitions": { + "NotHooked": { + "type": "object" + } + } + }`; + + const results = await s.run(doc); + + expect(results).toEqual([ + expect.objectContaining({ + code: 'invalid-ref', + path: ['x-hook', '$ref'], + }), + { + code: 'oas2-unused-definition', + message: 'Potentially unused definition has been detected.', + path: ['definitions', 'NotHooked'], + range: { + end: { + character: 26, + line: 9, + }, + start: { + character: 20, + line: 8, + }, + }, + severity: DiagnosticSeverity.Warning, + }, + ]); + }); + + test('does not report anything when all the definitions are referenced', async () => { + const doc = `{ + "swagger": "2.0", + "x-hook": { + "$ref": "#/definitions/Hooked" + }, + "x-also-hook": { + "$ref": "#/definitions/Hooked" + }, + "paths": { + "/path": { + "post": { + "parameters": [ + { + "$ref": "#/definitions/HookedAsWell" + } + ] + } + } + }, + "definitions": { + "Hooked": { + "type": "object" + }, + "HookedAsWell": { + "name": "value", + "in": "query", + "type": "number" + } + } + }`; + + const results = await s.run({ + parsed: parseWithPointers(doc), + getLocationForJsonPath, + }); + + expect(results).toEqual([]); + }); + + test('reports orphaned definitions', async () => { + const doc = `{ + "swagger": "2.0", + "paths": { + "/path": { + "post": {} + } + }, + "definitions": { + "BouhouhouIamUnused": { + "type": "object" + } + } + }`; + + const results = await s.run({ + parsed: parseWithPointers(doc), + getLocationForJsonPath, + }); + + expect(results).toEqual([ + { + code: 'oas2-unused-definition', + message: 'Potentially unused definition has been detected.', + path: ['definitions', 'BouhouhouIamUnused'], + range: { + end: { + character: 9, + line: 10, + }, + start: { + character: 30, + line: 8, + }, + }, + severity: DiagnosticSeverity.Warning, + }, + ]); + }); +}); diff --git a/src/rulesets/oas2/__tests__/valid-example-in-definitions.ts b/src/rulesets/oas/__tests__/oas2-valid-definition-example.ts similarity index 87% rename from src/rulesets/oas2/__tests__/valid-example-in-definitions.ts rename to src/rulesets/oas/__tests__/oas2-valid-definition-example.ts index 0762f38d7..bc18d965d 100644 --- a/src/rulesets/oas2/__tests__/valid-example-in-definitions.ts +++ b/src/rulesets/oas/__tests__/oas2-valid-definition-example.ts @@ -2,17 +2,13 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { RuleType, Spectral } from '../../../spectral'; import * as ruleset from '../index.json'; -// @oclif/test packages requires @types/mocha, therefore we have 2 packages coming up with similar typings -// TS is confused and prefers the mocha ones, so we need to instrument it to pick up the Jest ones -declare var test: jest.It; - -describe('valid-example-in-definitions', () => { +describe('oas2-valid-definition-example', () => { const s = new Spectral(); - + s.registerFormat('oas2', () => true); s.setRules({ - 'valid-example-in-definitions': Object.assign(ruleset.rules['valid-example-in-definitions'], { + 'oas2-valid-definition-example': Object.assign(ruleset.rules['oas2-valid-definition-example'], { recommended: true, - type: RuleType[ruleset.rules['valid-example-in-definitions'].type], + type: RuleType[ruleset.rules['oas2-valid-definition-example'].type], }), }); @@ -43,8 +39,8 @@ describe('valid-example-in-definitions', () => { }); expect(results).toEqual([ expect.objectContaining({ - code: 'valid-example-in-definitions', - message: '"xoxo.example" property type should be string', + code: 'oas2-valid-definition-example', + message: `\`example\` property type should be string`, severity: DiagnosticSeverity.Error, }), ]); @@ -107,8 +103,8 @@ describe('valid-example-in-definitions', () => { expect(results).toEqual([ expect.objectContaining({ - code: 'valid-example-in-definitions', - message: '"xoxo.example" property should have required property \'url\'', + code: 'oas2-valid-definition-example', + message: 'object should have required property `url`', severity: DiagnosticSeverity.Error, }), ]); @@ -184,8 +180,8 @@ describe('valid-example-in-definitions', () => { expect(results).toEqual([ expect.objectContaining({ severity: DiagnosticSeverity.Error, - code: 'valid-example-in-definitions', - message: '"halRoot.example" property type should be array', + code: 'oas2-valid-definition-example', + message: '`self` property type should be array', path: ['definitions', 'halRoot', 'example', '_links', 'self'], }), ]); @@ -232,8 +228,8 @@ describe('valid-example-in-definitions', () => { expect(results).toEqual([ expect.objectContaining({ - code: 'valid-example-in-definitions', - message: '"c.example" property type should be string', + code: 'oas2-valid-definition-example', + message: '`example` property type should be string', path: ['definitions', '0', 'xoxo', 'properties', 'a', 'properties', 'b', 'properties', 'c', 'example'], range: expect.any(Object), severity: DiagnosticSeverity.Error, diff --git a/src/rulesets/oas2/__tests__/valid-example-in-parameters.ts b/src/rulesets/oas/__tests__/oas2-valid-parameter-example.ts similarity index 87% rename from src/rulesets/oas2/__tests__/valid-example-in-parameters.ts rename to src/rulesets/oas/__tests__/oas2-valid-parameter-example.ts index da15882c4..e3cd24c88 100644 --- a/src/rulesets/oas2/__tests__/valid-example-in-parameters.ts +++ b/src/rulesets/oas/__tests__/oas2-valid-parameter-example.ts @@ -2,16 +2,13 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { RuleType, Spectral } from '../../../spectral'; import * as ruleset from '../index.json'; -// @oclif/test packages requires @types/mocha, therefore we have 2 packages coming up with similar typings -// TS is confused and prefers the mocha ones, so we need to instrument it to pick up the Jest ones -declare var test: jest.It; - -describe('valid-example-in-parameters', () => { +describe('oas2-valid-parameter-example', () => { const s = new Spectral(); + s.registerFormat('oas2', () => true); s.setRules({ - 'valid-example-in-parameters': Object.assign(ruleset.rules['valid-example-in-parameters'], { + 'oas2-valid-parameter-example': Object.assign(ruleset.rules['oas2-valid-parameter-example'], { recommended: true, - type: RuleType[ruleset.rules['valid-example-in-parameters'].type], + type: RuleType[ruleset.rules['oas2-valid-parameter-example'].type], }), }); @@ -59,8 +56,8 @@ describe('valid-example-in-parameters', () => { }); expect(results).toEqual([ expect.objectContaining({ - code: 'valid-example-in-parameters', - message: '"schema.example" property type should be string', + code: 'oas2-valid-parameter-example', + message: '`example` property type should be string', severity: DiagnosticSeverity.Error, }), ]); @@ -129,8 +126,8 @@ describe('valid-example-in-parameters', () => { expect(results).toEqual([ expect.objectContaining({ - code: 'valid-example-in-parameters', - message: '"schema.example" property should have required property \'url\'', + code: 'oas2-valid-parameter-example', + message: 'object should have required property `url`', severity: DiagnosticSeverity.Error, }), ]); @@ -196,8 +193,8 @@ describe('valid-example-in-parameters', () => { expect(results).toEqual([ expect.objectContaining({ - code: 'valid-example-in-parameters', - message: '"c.example" property type should be string', + code: 'oas2-valid-parameter-example', + message: '`example` property type should be string', path: [ 'paths', '/pet', diff --git a/src/rulesets/oas3/__tests__/api-servers.ts b/src/rulesets/oas/__tests__/oas3-api-servers.ts similarity index 84% rename from src/rulesets/oas3/__tests__/api-servers.ts rename to src/rulesets/oas/__tests__/oas3-api-servers.ts index a2971518c..9f183d549 100644 --- a/src/rulesets/oas3/__tests__/api-servers.ts +++ b/src/rulesets/oas/__tests__/oas3-api-servers.ts @@ -2,12 +2,13 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { RuleType, Spectral } from '../../../spectral'; import * as ruleset from '../index.json'; -describe('api-servers', () => { +describe('oas3-api-servers', () => { const s = new Spectral(); + s.registerFormat('oas3', () => true); s.setRules({ - 'api-servers': Object.assign(ruleset.rules['api-servers'], { + 'oas3-api-servers': Object.assign(ruleset.rules['oas3-api-servers'], { recommended: true, - type: RuleType[ruleset.rules['api-servers'].type], + type: RuleType[ruleset.rules['oas3-api-servers'].type], }), }); @@ -27,7 +28,7 @@ describe('api-servers', () => { }); expect(results).toEqual([ { - code: 'api-servers', + code: 'oas3-api-servers', message: 'OpenAPI `servers` must be present and non-empty array.', path: [], range: { @@ -53,7 +54,7 @@ describe('api-servers', () => { }); expect(results).toEqual([ { - code: 'api-servers', + code: 'oas3-api-servers', message: 'OpenAPI `servers` must be present and non-empty array.', path: ['servers'], range: { diff --git a/src/rulesets/oas3/__tests__/oas3-parameter-description.ts b/src/rulesets/oas/__tests__/oas3-parameter-description.ts similarity index 98% rename from src/rulesets/oas3/__tests__/oas3-parameter-description.ts rename to src/rulesets/oas/__tests__/oas3-parameter-description.ts index 908b622b3..0c76f7e13 100644 --- a/src/rulesets/oas3/__tests__/oas3-parameter-description.ts +++ b/src/rulesets/oas/__tests__/oas3-parameter-description.ts @@ -5,7 +5,7 @@ import * as ruleset from '../index.json'; describe('oas3-parameter-description', () => { const s = new Spectral(); - + s.registerFormat('oas3', () => true); s.setRules({ 'oas3-parameter-description': Object.assign(ruleset.rules['oas3-parameter-description'], { recommended: true, diff --git a/src/rulesets/oas3/__tests__/server-not-example.ts b/src/rulesets/oas/__tests__/oas3-server-not-example.ts similarity index 68% rename from src/rulesets/oas3/__tests__/server-not-example.ts rename to src/rulesets/oas/__tests__/oas3-server-not-example.ts index 6a227d40c..740b2ebca 100644 --- a/src/rulesets/oas3/__tests__/server-not-example.ts +++ b/src/rulesets/oas/__tests__/oas3-server-not-example.ts @@ -1,12 +1,13 @@ import { RuleType, Spectral } from '../../../spectral'; import * as ruleset from '../index.json'; -describe('server-not-example.com', () => { +describe('oas3-server-not-example.com', () => { const s = new Spectral(); + s.registerFormat('oas3', () => true); s.setRules({ - 'server-not-example.com': Object.assign(ruleset.rules['server-not-example.com'], { + 'oas3-server-not-example.com': Object.assign(ruleset.rules['oas3-server-not-example.com'], { recommended: true, - type: RuleType[ruleset.rules['server-not-example.com'].type], + type: RuleType[ruleset.rules['oas3-server-not-example.com'].type], }), }); @@ -35,8 +36,8 @@ describe('server-not-example.com', () => { }); expect(results).toEqual([ expect.objectContaining({ - code: 'server-not-example.com', - message: 'Server URL should not point at `example.com`.', + code: 'oas3-server-not-example.com', + message: 'Server URL should not point at example.com.', path: ['servers', '0', 'url'], }), ]); diff --git a/src/rulesets/oas3/__tests__/server-trailing-slash.ts b/src/rulesets/oas/__tests__/oas3-server-trailing-slash.ts similarity index 79% rename from src/rulesets/oas3/__tests__/server-trailing-slash.ts rename to src/rulesets/oas/__tests__/oas3-server-trailing-slash.ts index 0ce93044f..bfc2d00fd 100644 --- a/src/rulesets/oas3/__tests__/server-trailing-slash.ts +++ b/src/rulesets/oas/__tests__/oas3-server-trailing-slash.ts @@ -2,12 +2,13 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { RuleType, Spectral } from '../../../spectral'; import * as ruleset from '../index.json'; -describe('server-trailing-slash', () => { +describe('oas3-server-trailing-slash', () => { const s = new Spectral(); + s.registerFormat('oas3', () => true); s.setRules({ - 'server-trailing-slash': Object.assign(ruleset.rules['server-trailing-slash'], { + 'oas3-server-trailing-slash': Object.assign(ruleset.rules['oas3-server-trailing-slash'], { recommended: true, - type: RuleType[ruleset.rules['server-trailing-slash'].type], + type: RuleType[ruleset.rules['oas3-server-trailing-slash'].type], }), }); @@ -36,7 +37,7 @@ describe('server-trailing-slash', () => { }); expect(results).toEqual([ { - code: 'server-trailing-slash', + code: 'oas3-server-trailing-slash', message: 'Server URL should not have a trailing slash.', path: ['servers', '0', 'url'], range: { diff --git a/src/rulesets/oas/__tests__/oas3-unused-components-schema.jest.test.ts b/src/rulesets/oas/__tests__/oas3-unused-components-schema.jest.test.ts new file mode 100644 index 000000000..406d7c2f7 --- /dev/null +++ b/src/rulesets/oas/__tests__/oas3-unused-components-schema.jest.test.ts @@ -0,0 +1,172 @@ +import { getLocationForJsonPath, parseWithPointers } from '@stoplight/json'; +import * as path from '@stoplight/path'; +import { DiagnosticSeverity } from '@stoplight/types'; +import * as nock from 'nock'; + +import { readParsable } from '../../../fs/reader'; +import { unreferencedReusableObject } from '../../../functions/unreferencedReusableObject'; +import { IParsedResult, RuleType, Spectral } from '../../../index'; +import { httpAndFileResolver } from '../../../resolvers/http-and-file'; +import { rules } from '../index.json'; + +describe('unusedComponentsSchema - Http and fs remote references', () => { + const s = new Spectral({ resolver: httpAndFileResolver }); + s.registerFormat('oas3', () => true); + s.setFunctions({ unreferencedReusableObject }); + s.setRules({ + 'oas3-unused-components-schema': Object.assign(rules['oas3-unused-components-schema'], { + recommended: true, + type: RuleType[rules['oas3-unused-components-schema'].type], + }), + }); + + describe('reports unreferenced components schemas', () => { + test('when analyzing an in-memory document', async () => { + nock('https://oas3.library.com') + .get('/defs.json') + .reply( + 200, + JSON.stringify({ + components: { + schemas: { + ExternalHttp: { + type: 'number', + }, + }, + }, + }), + ); + + const remoteFsRefeferencePath = path.join( + __dirname, + '../../__tests__/__fixtures__/unusedComponentsSchema.definition.json#/components/schemas/ExternalFs', + ); + + const doc = `{ + "openapi": "3.0.0", + "x-hook": { + "$ref": "#/components/schemas/Hooked" + }, + "x-also-hook": { + "$ref": "#/components/schemas/Hooked" + }, + "paths": { + "/path": { + "post": { + "parameters": [ + { + "$ref": "#/components/schemas/HookedAsWell" + }, + { + "$ref": "${remoteFsRefeferencePath}" + }, + { + "$ref": "https://oas3.library.com/defs.json#/components/schemas/ExternalHttp" + } + ] + } + } + }, + "components": { + "schemas": { + "Hooked": { + "type": "object" + }, + "HookedAsWell": { + "name": "value", + "in": "query", + "type": "number" + }, + "Unhooked": { + "type": "object" + } + } + } + }`; + + const results = await s.run({ + parsed: parseWithPointers(doc), + getLocationForJsonPath, + }); + + expect(results).toEqual([ + { + code: 'oas3-unused-components-schema', + message: 'Potentially unused components schema has been detected.', + path: ['components', 'schemas', 'Unhooked'], + range: { + end: { + character: 11, + line: 37, + }, + start: { + character: 22, + line: 35, + }, + }, + severity: DiagnosticSeverity.Warning, + }, + ]); + + nock.cleanAll(); + }); + + test('when analyzing a directly self-referencing document from the filesystem', async () => { + const fixturePath = path.join(__dirname, '../../__tests__/__fixtures__/unusedComponentsSchema.remoteLocal.json'); + + const spec = parseWithPointers(await readParsable(fixturePath, { encoding: 'utf8' })); + + const parsedResult: IParsedResult = { + source: fixturePath, + parsed: spec, + getLocationForJsonPath, + }; + + const results = await s.run(parsedResult, { + resolve: { + documentUri: fixturePath, + }, + }); + + expect(results).toEqual([]); + }); + + test('when analyzing an indirectly self-referencing document from the filesystem', async () => { + const fixturePath = path.join(__dirname, '../../__tests__/__fixtures__/unusedComponentsSchema.indirect.1.json'); + + const spec = parseWithPointers(await readParsable(fixturePath, { encoding: 'utf8' })); + + const parsedResult: IParsedResult = { + source: fixturePath, + parsed: spec, + getLocationForJsonPath, + }; + + const results = await s.run(parsedResult, { + resolve: { + documentUri: fixturePath, + }, + }); + + expect(results).toEqual([ + { + code: 'oas3-unused-components-schema', + message: 'Potentially unused components schema has been detected.', + path: ['components', 'schemas', 'Unhooked'], + range: { + end: { + character: 7, + line: 12, + }, + start: { + character: 18, + line: 10, + }, + }, + severity: DiagnosticSeverity.Warning, + source: expect.stringMatching('/__tests__/__fixtures__/unusedComponentsSchema.indirect.1.json$'), + }, + ]); + }); + }); +}); diff --git a/src/rulesets/oas/__tests__/oas3-unused-components-schema.karma.test.ts b/src/rulesets/oas/__tests__/oas3-unused-components-schema.karma.test.ts new file mode 100644 index 000000000..ec005dc97 --- /dev/null +++ b/src/rulesets/oas/__tests__/oas3-unused-components-schema.karma.test.ts @@ -0,0 +1,108 @@ +import { getLocationForJsonPath, parseWithPointers } from '@stoplight/json'; +import { DiagnosticSeverity } from '@stoplight/types'; +import { FetchMockSandbox } from 'fetch-mock'; +import { unreferencedReusableObject } from '../../../functions/unreferencedReusableObject'; +import { RuleType, Spectral } from '../../../index'; +import { httpAndFileResolver } from '../../../resolvers/http-and-file'; +import { rules } from '../index.json'; + +describe('unusedComponentsSchema - Http remote references', () => { + let fetchMock: FetchMockSandbox; + + const s = new Spectral({ resolver: httpAndFileResolver }); + s.registerFormat('oas3', () => true); + s.setFunctions({ unreferencedReusableObject }); + s.setRules({ + 'oas3-unused-components-schema': Object.assign(rules['oas3-unused-components-schema'], { + recommended: true, + type: RuleType[rules['oas3-unused-components-schema'].type], + }), + }); + + beforeEach(() => { + fetchMock = require('fetch-mock').sandbox(); + window.fetch = fetchMock; + }); + + afterEach(() => { + window.fetch = fetch; + }); + + test('reports unreferenced components schemas', async () => { + fetchMock.mock('https://oas3.library.com/defs.json', { + status: 200, + body: { + components: { + schemas: { + ExternalHttp: { + type: 'number', + }, + }, + }, + }, + }); + + const doc = `{ + "openapi": "3.0.0", + "x-hook": { + "$ref": "#/components/schemas/Hooked" + }, + "x-also-hook": { + "$ref": "#/components/schemas/Hooked" + }, + "paths": { + "/path": { + "post": { + "parameters": [ + { + "$ref": "#/components/schemas/HookedAsWell" + }, + { + "$ref": "https://oas3.library.com/defs.json#/components/schemas/ExternalHttp" + } + ] + } + } + }, + "components": { + "schemas": { + "Hooked": { + "type": "object" + }, + "HookedAsWell": { + "name": "value", + "in": "query", + "type": "number" + }, + "Unhooked": { + "type": "object" + } + } + } + }`; + + const results = await s.run({ + parsed: parseWithPointers(doc), + getLocationForJsonPath, + }); + + expect(results).toEqual([ + { + code: 'oas3-unused-components-schema', + message: 'Potentially unused components schema has been detected.', + path: ['components', 'schemas', 'Unhooked'], + range: { + end: { + character: 11, + line: 34, + }, + start: { + character: 22, + line: 32, + }, + }, + severity: DiagnosticSeverity.Warning, + }, + ]); + }); +}); diff --git a/src/rulesets/oas/__tests__/oas3-unused-components-schema.test.ts b/src/rulesets/oas/__tests__/oas3-unused-components-schema.test.ts new file mode 100644 index 000000000..c3fb7b90f --- /dev/null +++ b/src/rulesets/oas/__tests__/oas3-unused-components-schema.test.ts @@ -0,0 +1,153 @@ +import { getLocationForJsonPath, parseWithPointers } from '@stoplight/json'; +import { DiagnosticSeverity } from '@stoplight/types'; +import { unreferencedReusableObject } from '../../../functions/unreferencedReusableObject'; +import { RuleType, Spectral } from '../../../index'; +import { httpAndFileResolver } from '../../../resolvers/http-and-file'; +import { rules } from '../index.json'; + +describe('unusedComponentsSchema - Local references', () => { + const s = new Spectral({ resolver: httpAndFileResolver }); + s.registerFormat('oas3', () => true); + s.setFunctions({ unreferencedReusableObject }); + s.setRules({ + 'oas3-unused-components-schema': Object.assign(rules['oas3-unused-components-schema'], { + recommended: true, + type: RuleType[rules['oas3-unused-components-schema'].type], + }), + }); + + test('does not report anything for empty object', async () => { + const results = await s.run({ + openapi: '3.0.0', + }); + + expect(results).toEqual([]); + }); + + test('does not throw when meeting an invalid json pointer', async () => { + const doc = `{ + "openapi": "3.0.0", + "x-hook": { + "$ref": "'$#@!!!' What?" + }, + "paths": { + }, + "components": { + "schemas": { + "NotHooked": { + "type": "object" + } + } + } + }`; + + const results = await s.run(doc); + + expect(results).toEqual([ + expect.objectContaining({ + code: 'invalid-ref', + path: ['x-hook', '$ref'], + }), + { + code: 'oas3-unused-components-schema', + message: 'Potentially unused components schema has been detected.', + path: ['components', 'schemas', 'NotHooked'], + range: { + end: { + character: 28, + line: 10, + }, + start: { + character: 22, + line: 9, + }, + }, + severity: DiagnosticSeverity.Warning, + }, + ]); + }); + + test('does not report anything when all the components schemas are referenced', async () => { + const doc = `{ + "openapi": "3.0.0", + "x-hook": { + "$ref": "#/components/schemas/Hooked" + }, + "x-also-hook": { + "$ref": "#/components/schemas/Hooked" + }, + "paths": { + "/path": { + "post": { + "parameters": [ + { + "$ref": "#/components/schemas/HookedAsWell" + } + ] + } + } + }, + "components": { + "schemas": { + "Hooked": { + "type": "object" + }, + "HookedAsWell": { + "name": "value", + "in": "query", + "type": "number" + } + } + } + }`; + + const results = await s.run({ + parsed: parseWithPointers(doc), + getLocationForJsonPath, + }); + + expect(results).toEqual([]); + }); + + test('reports orphaned components schemas', async () => { + const doc = `{ + "openapi": "3.0.0", + "paths": { + "/path": { + "post": {} + } + }, + "components": { + "schemas": { + "BouhouhouIamUnused": { + "type": "object" + } + } + } + }`; + + const results = await s.run({ + parsed: parseWithPointers(doc), + getLocationForJsonPath, + }); + + expect(results).toEqual([ + { + code: 'oas3-unused-components-schema', + message: 'Potentially unused components schema has been detected.', + path: ['components', 'schemas', 'BouhouhouIamUnused'], + range: { + end: { + character: 11, + line: 11, + }, + start: { + character: 32, + line: 9, + }, + }, + severity: DiagnosticSeverity.Warning, + }, + ]); + }); +}); diff --git a/src/rulesets/oas3/__tests__/valid-schema-example-in-content.ts b/src/rulesets/oas/__tests__/oas3-valid-content-schema-example.ts similarity index 67% rename from src/rulesets/oas3/__tests__/valid-schema-example-in-content.ts rename to src/rulesets/oas/__tests__/oas3-valid-content-schema-example.ts index 94189d58d..28596f4fa 100644 --- a/src/rulesets/oas3/__tests__/valid-schema-example-in-content.ts +++ b/src/rulesets/oas/__tests__/oas3-valid-content-schema-example.ts @@ -1,5 +1,5 @@ import testRule from './templates/_schema-example'; -const ruleName = 'valid-schema-example-in-content'; +const ruleName = 'oas3-valid-content-schema-example'; describe(ruleName, () => testRule(ruleName, 'content')); diff --git a/src/rulesets/oas3/__tests__/valid-schema-example-in-headers.ts b/src/rulesets/oas/__tests__/oas3-valid-header-schema-example.ts similarity index 67% rename from src/rulesets/oas3/__tests__/valid-schema-example-in-headers.ts rename to src/rulesets/oas/__tests__/oas3-valid-header-schema-example.ts index 075cb9e18..674b0e3db 100644 --- a/src/rulesets/oas3/__tests__/valid-schema-example-in-headers.ts +++ b/src/rulesets/oas/__tests__/oas3-valid-header-schema-example.ts @@ -1,5 +1,5 @@ import testRule from './templates/_schema-example'; -const ruleName = 'valid-schema-example-in-headers'; +const ruleName = 'oas3-valid-header-schema-example'; describe(ruleName, () => testRule(ruleName, 'headers')); diff --git a/src/rulesets/oas3/__tests__/valid-oas-example-in-content.ts b/src/rulesets/oas/__tests__/oas3-valid-oas-content-example.ts similarity index 67% rename from src/rulesets/oas3/__tests__/valid-oas-example-in-content.ts rename to src/rulesets/oas/__tests__/oas3-valid-oas-content-example.ts index a4a347fdb..6929c9416 100644 --- a/src/rulesets/oas3/__tests__/valid-oas-example-in-content.ts +++ b/src/rulesets/oas/__tests__/oas3-valid-oas-content-example.ts @@ -1,5 +1,5 @@ import testRule from './templates/_oas-example'; -const ruleName = 'valid-oas-example-in-content'; +const ruleName = 'oas3-valid-oas-content-example'; describe(ruleName, () => testRule(ruleName, 'content')); diff --git a/src/rulesets/oas3/__tests__/valid-oas-example-in-headers.ts b/src/rulesets/oas/__tests__/oas3-valid-oas-header-example.ts similarity index 68% rename from src/rulesets/oas3/__tests__/valid-oas-example-in-headers.ts rename to src/rulesets/oas/__tests__/oas3-valid-oas-header-example.ts index b65f33cd1..10569e870 100644 --- a/src/rulesets/oas3/__tests__/valid-oas-example-in-headers.ts +++ b/src/rulesets/oas/__tests__/oas3-valid-oas-header-example.ts @@ -1,5 +1,5 @@ import testRule from './templates/_oas-example'; -const ruleName = 'valid-oas-example-in-headers'; +const ruleName = 'oas3-valid-oas-header-example'; describe(ruleName, () => testRule(ruleName, 'headers')); diff --git a/src/rulesets/oas3/__tests__/valid-oas-example-in-parameters.ts b/src/rulesets/oas/__tests__/oas3-valid-oas-parameter-example.ts similarity index 79% rename from src/rulesets/oas3/__tests__/valid-oas-example-in-parameters.ts rename to src/rulesets/oas/__tests__/oas3-valid-oas-parameter-example.ts index 96e7a91b5..839896a35 100644 --- a/src/rulesets/oas3/__tests__/valid-oas-example-in-parameters.ts +++ b/src/rulesets/oas/__tests__/oas3-valid-oas-parameter-example.ts @@ -2,17 +2,13 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { RuleType, Spectral } from '../../../spectral'; import * as ruleset from '../index.json'; -// @oclif/test packages requires @types/mocha, therefore we have 2 packages coming up with similar typings -// TS is confused and prefers the mocha ones, so we need to instrument it to pick up the Jest ones -declare var test: jest.It; - -describe('valid-oas-example-in-parameters', () => { +describe('oas3-valid-oas-parameter-example', () => { const s = new Spectral(); - + s.registerFormat('oas3', () => true); s.setRules({ - 'valid-oas-example-in-parameters': Object.assign(ruleset.rules['valid-oas-example-in-parameters'], { + 'oas3-valid-oas-parameter-example': Object.assign(ruleset.rules['oas3-valid-oas-parameter-example'], { recommended: true, - type: RuleType[ruleset.rules['valid-oas-example-in-parameters'].type], + type: RuleType[ruleset.rules['oas3-valid-oas-parameter-example'].type], }), }); @@ -44,8 +40,8 @@ describe('valid-oas-example-in-parameters', () => { expect(results).toEqual([ expect.objectContaining({ severity: DiagnosticSeverity.Error, - code: 'valid-oas-example-in-parameters', - message: '"0.example" property type should be string', + code: 'oas3-valid-oas-parameter-example', + message: '`example` property type should be string', }), ]); }); @@ -122,8 +118,8 @@ describe('valid-oas-example-in-parameters', () => { expect(results).toEqual([ expect.objectContaining({ - code: 'valid-oas-example-in-parameters', - message: '"Abc.example" property should have required property \'abc\'', + code: 'oas3-valid-oas-parameter-example', + message: 'object should have required property `abc`', severity: DiagnosticSeverity.Error, }), ]); @@ -159,8 +155,8 @@ describe('valid-oas-example-in-parameters', () => { expect(results).toEqual([ expect.objectContaining({ - code: 'valid-oas-example-in-parameters', - message: '"0.example" property should have required property \'url\'', + code: 'oas3-valid-oas-parameter-example', + message: 'object should have required property `url`', severity: DiagnosticSeverity.Error, }), ]); diff --git a/src/rulesets/oas3/__tests__/valid-schema-example-in-parameters.ts b/src/rulesets/oas/__tests__/oas3-valid-parameter-schema-example.ts similarity index 86% rename from src/rulesets/oas3/__tests__/valid-schema-example-in-parameters.ts rename to src/rulesets/oas/__tests__/oas3-valid-parameter-schema-example.ts index b89d52a42..8067d9b98 100644 --- a/src/rulesets/oas3/__tests__/valid-schema-example-in-parameters.ts +++ b/src/rulesets/oas/__tests__/oas3-valid-parameter-schema-example.ts @@ -2,17 +2,13 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { RuleType, Spectral } from '../../../spectral'; import * as ruleset from '../index.json'; -// @oclif/test packages requires @types/mocha, therefore we have 2 packages coming up with similar typings -// TS is confused and prefers the mocha ones, so we need to instrument it to pick up the Jest ones -declare var test: jest.It; - -describe('valid-schema-example-in-parameters', () => { +describe('oas3-valid-parameter-schema-example', () => { const s = new Spectral(); - + s.registerFormat('oas3', () => true); s.setRules({ - 'valid-schema-example-in-parameters': Object.assign(ruleset.rules['valid-schema-example-in-parameters'], { + 'oas3-valid-parameter-schema-example': Object.assign(ruleset.rules['oas3-valid-parameter-schema-example'], { recommended: true, - type: RuleType[ruleset.rules['valid-schema-example-in-parameters'].type], + type: RuleType[ruleset.rules['oas3-valid-parameter-schema-example'].type], }), }); @@ -90,8 +86,8 @@ describe('valid-schema-example-in-parameters', () => { expect(results).toEqual([ expect.objectContaining({ - code: 'valid-schema-example-in-parameters', - message: '"c.example" property type should be string', + code: 'oas3-valid-parameter-schema-example', + message: '`example` property type should be string', path: [ 'paths', '/pet', @@ -172,8 +168,8 @@ describe('valid-schema-example-in-parameters', () => { expect(results).toEqual([ expect.objectContaining({ severity: DiagnosticSeverity.Error, - code: 'valid-schema-example-in-parameters', - message: '"schema.example" property type should be string', + code: 'oas3-valid-parameter-schema-example', + message: '`example` property type should be string', }), ]); }); @@ -250,8 +246,8 @@ describe('valid-schema-example-in-parameters', () => { expect(results).toEqual([ expect.objectContaining({ - code: 'valid-schema-example-in-parameters', - message: '"abc.example" property type should be number', + code: 'oas3-valid-parameter-schema-example', + message: '`example` property type should be number', severity: DiagnosticSeverity.Error, }), ]); @@ -287,8 +283,8 @@ describe('valid-schema-example-in-parameters', () => { expect(results).toEqual([ expect.objectContaining({ - code: 'valid-schema-example-in-parameters', - message: '"schema.example" property should have required property \'url\'', + code: 'oas3-valid-parameter-schema-example', + message: 'object should have required property `url`', severity: DiagnosticSeverity.Error, }), ]); diff --git a/src/rulesets/oas3/__tests__/valid-example-in-schemas.ts b/src/rulesets/oas/__tests__/oas3-valid-schema-example.ts similarity index 84% rename from src/rulesets/oas3/__tests__/valid-example-in-schemas.ts rename to src/rulesets/oas/__tests__/oas3-valid-schema-example.ts index 9052ae7ee..eff607011 100644 --- a/src/rulesets/oas3/__tests__/valid-example-in-schemas.ts +++ b/src/rulesets/oas/__tests__/oas3-valid-schema-example.ts @@ -2,22 +2,19 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { RuleType, Spectral } from '../../../spectral'; import * as ruleset from '../index.json'; -// @oclif/test packages requires @types/mocha, therefore we have 2 packages coming up with similar typings -// TS is confused and prefers the mocha ones, so we need to instrument it to pick up the Jest ones -declare var test: jest.It; - -describe('valid-example-in-schemas', () => { +describe('oas3-valid-schema-example', () => { const s = new Spectral(); - + s.registerFormat('oas3', () => true); s.setRules({ - 'valid-example-in-schemas': Object.assign(ruleset.rules['valid-example-in-schemas'], { + 'oas3-valid-schema-example': Object.assign(ruleset.rules['oas3-valid-schema-example'], { recommended: true, - type: RuleType[ruleset.rules['valid-example-in-schemas'].type], + type: RuleType[ruleset.rules['oas3-valid-schema-example']!.type], }), }); test('will pass when simple example is valid', async () => { const results = await s.run({ + openapi: '3.0.2', components: { schemas: { xoxo: { @@ -32,6 +29,7 @@ describe('valid-example-in-schemas', () => { test('will fail when simple example is invalid', async () => { const results = await s.run({ + openapi: '3.0.2', components: { schemas: { xoxo: { @@ -44,15 +42,15 @@ describe('valid-example-in-schemas', () => { expect(results).toEqual([ expect.objectContaining({ severity: DiagnosticSeverity.Error, - code: 'valid-example-in-schemas', - message: '"xoxo.example" property type should be string', + code: 'oas3-valid-schema-example', + message: '`example` property type should be string', }), ]); }); test('will pass for valid parents examples which contain invalid child examples', async () => { const results = await s.run({ - swagger: '2.0', + openapi: '3.0.2', info: { version: '1.0.0', title: 'Swagger Petstore', @@ -98,8 +96,8 @@ describe('valid-example-in-schemas', () => { expect(results).toEqual([ expect.objectContaining({ - code: 'valid-example-in-schemas', - message: '"c.example" property type should be string', + code: 'oas3-valid-schema-example', + message: '`example` property type should be string', path: [ 'components', 'schemas', @@ -121,6 +119,7 @@ describe('valid-example-in-schemas', () => { test('will pass when complex example is used ', async () => { const results = await s.run({ + openapi: '3.0.2', components: { schemas: { xoxo: { @@ -152,6 +151,7 @@ describe('valid-example-in-schemas', () => { test('will fail when complex example is used', async () => { const data = { + openapi: '3.0.2', components: { schemas: { xoxo: { @@ -187,8 +187,8 @@ describe('valid-example-in-schemas', () => { expect(results).toEqual([ expect.objectContaining({ - code: 'valid-example-in-schemas', - message: '"abc.example" property type should be number', + code: 'oas3-valid-schema-example', + message: '`example` property type should be number', severity: DiagnosticSeverity.Error, }), ]); @@ -196,6 +196,7 @@ describe('valid-example-in-schemas', () => { test('will error with totally invalid input', async () => { const results = await s.run({ + openapi: '3.0.2', components: { schemas: { xoxo: { @@ -224,8 +225,8 @@ describe('valid-example-in-schemas', () => { expect(results).toEqual([ expect.objectContaining({ - code: 'valid-example-in-schemas', - message: '"xoxo.example" property should have required property \'url\'', + code: 'oas3-valid-schema-example', + message: 'object should have required property `url`', severity: DiagnosticSeverity.Error, }), ]); diff --git a/src/rulesets/oas/__tests__/operation-summary-formatted.ts b/src/rulesets/oas/__tests__/operation-summary-formatted.ts deleted file mode 100644 index 2422134b5..000000000 --- a/src/rulesets/oas/__tests__/operation-summary-formatted.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { DiagnosticSeverity } from '@stoplight/types'; -import { RuleType, Spectral } from '../../../spectral'; -import * as ruleset from '../index.json'; - -describe('operation-summary-formatted', () => { - const s = new Spectral(); - s.setRules({ - 'operation-summary-formatted': Object.assign(ruleset.rules['operation-summary-formatted'], { - recommended: true, - type: RuleType[ruleset.rules['operation-summary-formatted'].type], - }), - }); - - test('validate a correct object', async () => { - const results = await s.run({ - swagger: '2.0', - paths: { - '/todos': { - get: { - summary: 'This is a valid summary.', - }, - }, - }, - }); - expect(results.length).toEqual(0); - }); - - test('return errors if summary does not start with an uppercase', async () => { - const results = await s.run({ - swagger: '2.0', - paths: { - '/todos': { - get: { - summary: 'this is not a valid summary.', - }, - }, - }, - }); - expect(results).toEqual([ - { - code: 'operation-summary-formatted', - message: 'Operation `summary` should start with upper case and end with a dot.', - path: ['paths', '/todos', 'get', 'summary'], - range: { - end: { - character: 49, - line: 5, - }, - start: { - character: 19, - line: 5, - }, - }, - severity: DiagnosticSeverity.Warning, - }, - ]); - }); - - test('return errors if summary does not end with a dot', async () => { - const results = await s.run({ - swagger: '2.0', - paths: { - '/todos': { - get: { - summary: 'This is not a valid summary', - }, - }, - }, - }); - expect(results).toEqual([ - { - code: 'operation-summary-formatted', - message: 'Operation `summary` should start with upper case and end with a dot.', - path: ['paths', '/todos', 'get', 'summary'], - range: { - end: { - character: 48, - line: 5, - }, - start: { - character: 19, - line: 5, - }, - }, - severity: DiagnosticSeverity.Warning, - }, - ]); - }); -}); diff --git a/src/rulesets/oas3/__tests__/templates/_oas-example.ts b/src/rulesets/oas/__tests__/templates/_oas-example.ts similarity index 67% rename from src/rulesets/oas3/__tests__/templates/_oas-example.ts rename to src/rulesets/oas/__tests__/templates/_oas-example.ts index 0d9064164..fd4b593ed 100644 --- a/src/rulesets/oas3/__tests__/templates/_oas-example.ts +++ b/src/rulesets/oas/__tests__/templates/_oas-example.ts @@ -1,23 +1,20 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { RuleType, Spectral } from '../../../../spectral'; -import * as ruleset from '../../index.json'; - -// @oclif/test packages requires @types/mocha, therefore we have 2 packages coming up with similar typings -// TS is confused and prefers the mocha ones, so we need to instrument it to pick up the Jest ones -declare var test: jest.It; +import { rules } from '../../index.json'; export default (ruleName: string, path: string) => { const s = new Spectral(); - + s.registerFormat('oas3', () => true); s.setRules({ - [ruleName]: Object.assign(ruleset.rules[ruleName], { + [ruleName]: Object.assign(rules[ruleName], { recommended: true, - type: RuleType[ruleset.rules[ruleName].type], + type: RuleType[rules[ruleName].type], }), }); test('will pass when simple example is valid', async () => { const results = await s.run({ + openapi: '3.0.0', [path]: { xoxo: { schema: { @@ -32,6 +29,7 @@ export default (ruleName: string, path: string) => { test('will fail when simple example is invalid', async () => { const results = await s.run({ + openapi: '3.0.0', [path]: { xoxo: { schema: { @@ -45,13 +43,14 @@ export default (ruleName: string, path: string) => { expect.objectContaining({ severity: DiagnosticSeverity.Error, code: ruleName, - message: '"xoxo.example" property type should be string', + message: '`example` property type should be string', }), ]); }); test('will pass when complex example is used ', async () => { const results = await s.run({ + openapi: '3.0.0', [path]: { xoxo: { schema: { @@ -83,6 +82,7 @@ export default (ruleName: string, path: string) => { test('will fail when complex example is used ', async () => { const data = { + openapi: '3.0.0', [path]: { xoxo: { schema: { @@ -123,6 +123,7 @@ export default (ruleName: string, path: string) => { test('will error with totally invalid input', async () => { const results = await s.run({ + openapi: '3.0.0', [path]: { xoxo: { schema: { @@ -157,7 +158,7 @@ export default (ruleName: string, path: string) => { expect(results).toEqual([ expect.objectContaining({ code: ruleName, - message: '"xoxo.example" property should have required property \'url\'', + message: 'object should have required property `url`', severity: DiagnosticSeverity.Error, }), ]); @@ -165,6 +166,7 @@ export default (ruleName: string, path: string) => { test('does not report example mismatches for unknown AJV formats', async () => { const results = await s.run({ + openapi: '3.0.0', [path]: { xoxo: { schema: { @@ -179,57 +181,64 @@ export default (ruleName: string, path: string) => { expect(results).toEqual([]); }); - test.each([['byte', '1'], ['int32', 2 ** 31], ['int64', 2 ** 63], ['float', 2 ** 128]])( - 'reports invalid usage of %s format', - async (format, example) => { - const results = await s.run({ - [path]: { - xoxo: { - schema: { - type: ['string', 'number'], - format, - properties: { - ip_address: { - type: 'string', - }, + test.each([ + ['byte', '1'], + ['int32', 2 ** 31], + ['int64', 2 ** 63], + ['float', 2 ** 128], + ])('reports invalid usage of %s format', async (format, example) => { + const results = await s.run({ + openapi: '3.0.0', + [path]: { + xoxo: { + schema: { + type: ['string', 'number'], + format, + properties: { + ip_address: { + type: 'string', }, }, - example, }, + example, }, - }); - - expect(results).toEqual([ - expect.objectContaining({ - severity: DiagnosticSeverity.Error, - code: ruleName, - message: `"xoxo.example" property format should match format "${format}"`, // hm, ip_address is likely to be more meaningful no? - }), - ]); - }, - ); - - test.each([['byte', 'MTI3'], ['int32', 2 ** 30], ['int64', 2 ** 40], ['float', 2 ** 64], ['double', 2 ** 1028]])( - 'does not report valid usage of %s format', - async (format, example) => { - const results = await s.run({ - [path]: { - xoxo: { - schema: { - type: ['string', 'number'], - format, - properties: { - ip_address: { - type: 'string', - }, + }, + }); + + expect(results).toEqual([ + expect.objectContaining({ + severity: DiagnosticSeverity.Error, + code: ruleName, + message: `\`example\` property format should match format \`${format}\``, + }), + ]); + }); + + test.each([ + ['byte', 'MTI3'], + ['int32', 2 ** 30], + ['int64', 2 ** 40], + ['float', 2 ** 64], + ['double', 2 ** 1028], + ])('does not report valid usage of %s format', async (format, example) => { + const results = await s.run({ + openapi: '3.0.0', + [path]: { + xoxo: { + schema: { + type: ['string', 'number'], + format, + properties: { + ip_address: { + type: 'string', }, }, - example, }, + example, }, - }); + }, + }); - expect(results).toHaveLength(0); - }, - ); + expect(results).toHaveLength(0); + }); }; diff --git a/src/rulesets/oas3/__tests__/templates/_schema-example.ts b/src/rulesets/oas/__tests__/templates/_schema-example.ts similarity index 72% rename from src/rulesets/oas3/__tests__/templates/_schema-example.ts rename to src/rulesets/oas/__tests__/templates/_schema-example.ts index 8d57384d5..2f7d698b0 100644 --- a/src/rulesets/oas3/__tests__/templates/_schema-example.ts +++ b/src/rulesets/oas/__tests__/templates/_schema-example.ts @@ -1,23 +1,20 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { RuleType, Spectral } from '../../../../spectral'; -import * as ruleset from '../../index.json'; - -// @oclif/test packages requires @types/mocha, therefore we have 2 packages coming up with similar typings -// TS is confused and prefers the mocha ones, so we need to instrument it to pick up the Jest ones -declare var test: jest.It; +import { rules } from '../../index.json'; export default (ruleName: string, path: string) => { const s = new Spectral(); - + s.registerFormat('oas3', () => true); s.setRules({ - [ruleName]: Object.assign(ruleset.rules[ruleName], { + [ruleName]: Object.assign(rules[ruleName], { recommended: true, - type: RuleType[ruleset.rules[ruleName].type], + type: RuleType[rules[ruleName].type], }), }); test('will pass when simple example is valid', async () => { const results = await s.run({ + openapi: '3.0.2', [path]: { xoxo: { schema: { @@ -32,6 +29,7 @@ export default (ruleName: string, path: string) => { test('will fail when simple example is invalid', async () => { const results = await s.run({ + openapi: '3.0.2', [path]: { xoxo: { schema: { @@ -45,13 +43,14 @@ export default (ruleName: string, path: string) => { expect.objectContaining({ severity: DiagnosticSeverity.Error, code: ruleName, - message: '"schema.example" property type should be string', + message: '`example` property type should be string', }), ]); }); test('will pass for valid parents examples which contain invalid child examples', async () => { const results = await s.run({ + openapi: '3.0.2', [path]: { xoxo: [ { @@ -94,7 +93,7 @@ export default (ruleName: string, path: string) => { expect(results).toEqual([ expect.objectContaining({ code: ruleName, - message: '"c.example" property type should be string', + message: '`example` property type should be string', path: [path, 'xoxo', '0', 'schema', 'properties', 'a', 'properties', 'b', 'properties', 'c', 'example'], range: expect.any(Object), severity: DiagnosticSeverity.Error, @@ -104,6 +103,7 @@ export default (ruleName: string, path: string) => { test('will pass when complex example is used ', async () => { const results = await s.run({ + openapi: '3.0.2', [path]: { xoxo: { schema: { @@ -135,6 +135,7 @@ export default (ruleName: string, path: string) => { test('will fail when complex example is used ', async () => { const data = { + openapi: '3.0.2', [path]: { Heh: { schema: { @@ -173,7 +174,7 @@ export default (ruleName: string, path: string) => { expect(results).toEqual([ expect.objectContaining({ code: ruleName, - message: '"abc.example" property type should be number', + message: '`example` property type should be number', severity: DiagnosticSeverity.Error, }), ]); @@ -181,6 +182,7 @@ export default (ruleName: string, path: string) => { test('will error with totally invalid input', async () => { const results = await s.run({ + openapi: '3.0.2', [path]: { xoxo: { schema: { @@ -210,7 +212,7 @@ export default (ruleName: string, path: string) => { expect(results).toEqual([ expect.objectContaining({ code: ruleName, - message: '"schema.example" property should have required property \'url\'', + message: 'object should have required property `url`', severity: DiagnosticSeverity.Error, }), ]); @@ -218,6 +220,7 @@ export default (ruleName: string, path: string) => { test('does not report example mismatches for unknown AJV formats', async () => { const results = await s.run({ + openapi: '3.0.2', [path]: { xoxo: { schema: { @@ -237,57 +240,64 @@ export default (ruleName: string, path: string) => { expect(results).toEqual([]); }); - test.each([['byte', '1'], ['int32', 2 ** 31], ['int64', 2 ** 63], ['float', 2 ** 128]])( - 'reports invalid usage of %s format', - async (format, example) => { - const results = await s.run({ - [path]: { - xoxo: { - schema: { - type: 'object', - properties: { - ip_address: { - type: ['string', 'number'], - format, - example, - }, + test.each([ + ['byte', '1'], + ['int32', 2 ** 31], + ['int64', 2 ** 63], + ['float', 2 ** 128], + ])('reports invalid usage of %s format', async (format, example) => { + const results = await s.run({ + openapi: '3.0.2', + [path]: { + xoxo: { + schema: { + type: 'object', + properties: { + ip_address: { + type: ['string', 'number'], + format, + example, }, }, }, }, - }); + }, + }); - expect(results).toEqual([ - expect.objectContaining({ - severity: DiagnosticSeverity.Error, - code: ruleName, - message: `"ip_address.example" property format should match format "${format}"`, // hm, ip_address is likely to be more meaningful no? - }), - ]); - }, - ); + expect(results).toEqual([ + expect.objectContaining({ + severity: DiagnosticSeverity.Error, + code: ruleName, + message: `\`example\` property format should match format \`${format}\``, + }), + ]); + }); - test.each([['byte', 'MTI3'], ['int32', 2 ** 30], ['int64', 2 ** 40], ['float', 2 ** 64], ['double', 2 ** 1028]])( - 'does not report valid usage of %s format', - async (format, example) => { - const results = await s.run({ - [path]: { - xoxo: { - schema: { - type: 'object', - properties: { - ip_address: { - type: ['string', 'number'], - format, - example, - }, + test.each([ + ['byte', 'MTI3'], + ['int32', 2 ** 30], + ['int64', 2 ** 40], + ['float', 2 ** 64], + ['double', 2 ** 1028], + ])('does not report valid usage of %s format', async (format, example) => { + const results = await s.run({ + openapi: '3.0.2', + [path]: { + xoxo: { + schema: { + type: 'object', + properties: { + ip_address: { + type: ['string', 'number'], + format, + example, }, }, }, }, - }); + }, + }); - expect(results).toHaveLength(0); - }, - ); + expect(results).toHaveLength(0); + }); }; diff --git a/src/rulesets/oas/functions/__tests__/oasOpFormDataConsumeCheck.test.ts b/src/rulesets/oas/functions/__tests__/oasOpFormDataConsumeCheck.test.ts index 423361030..38da524b6 100644 --- a/src/rulesets/oas/functions/__tests__/oasOpFormDataConsumeCheck.test.ts +++ b/src/rulesets/oas/functions/__tests__/oasOpFormDataConsumeCheck.test.ts @@ -5,16 +5,18 @@ import oasOpFormDataConsumeCheck from '../oasOpFormDataConsumeCheck'; describe('oasOpFormDataConsumeCheck', () => { const s = new Spectral(); + s.registerFormat('oas2', () => true); s.setFunctions({ oasOpFormDataConsumeCheck }); s.setRules({ - 'operation-formData-consume-check': Object.assign(rules['operation-formData-consume-check'], { + 'oas2-operation-formData-consume-check': Object.assign(rules['oas2-operation-formData-consume-check'], { recommended: true, - type: RuleType[rules['operation-formData-consume-check'].type], + type: RuleType[rules['oas2-operation-formData-consume-check'].type], }), }); test('validate a correct object', async () => { const results = await s.run({ + swagger: '2.0', paths: { '/path1': { get: { @@ -29,6 +31,7 @@ describe('oasOpFormDataConsumeCheck', () => { test('return errors on different path operations same id', async () => { const results = await s.run({ + swagger: '2.0', paths: { '/path1': { get: { @@ -41,18 +44,18 @@ describe('oasOpFormDataConsumeCheck', () => { expect(results).toEqual([ { - code: 'operation-formData-consume-check', + code: 'oas2-operation-formData-consume-check', message: 'Operations with an `in: formData` parameter must include `application/x-www-form-urlencoded` or `multipart/form-data` in their `consumes` property.', path: ['paths', '/path1', 'get'], range: { end: { character: 26, - line: 10, + line: 11, }, start: { character: 12, - line: 3, + line: 4, }, }, severity: DiagnosticSeverity.Warning, diff --git a/src/rulesets/oas/functions/__tests__/oasOpParams.test.ts b/src/rulesets/oas/functions/__tests__/oasOpParams.test.ts index 8c587e7f1..1fe2674e7 100644 --- a/src/rulesets/oas/functions/__tests__/oasOpParams.test.ts +++ b/src/rulesets/oas/functions/__tests__/oasOpParams.test.ts @@ -58,7 +58,11 @@ describe('oasOpParams', () => { paths: { '/foo': { get: { - parameters: [{ in: 'query', name: 'foo' }, { in: 'query', name: 'foo' }, { in: 'query', name: 'foo' }], + parameters: [ + { in: 'query', name: 'foo' }, + { in: 'query', name: 'foo' }, + { in: 'query', name: 'foo' }, + ], }, put: {}, }, @@ -140,7 +144,7 @@ describe('oasOpParams', () => { }, }, }); - expect(results.length).toEqual(2); + expect(results.length).toEqual(1); }); test('Error if multiple in:body', async () => { @@ -148,7 +152,10 @@ describe('oasOpParams', () => { paths: { '/foo': { get: { - parameters: [{ in: 'body', name: 'foo' }, { in: 'body', name: 'bar' }], + parameters: [ + { in: 'body', name: 'foo' }, + { in: 'body', name: 'bar' }, + ], }, put: {}, }, @@ -179,7 +186,10 @@ describe('oasOpParams', () => { paths: { '/foo': { get: { - parameters: [{ in: 'body', name: 'foo' }, { in: 'formData', name: 'bar' }], + parameters: [ + { in: 'body', name: 'foo' }, + { in: 'formData', name: 'bar' }, + ], }, }, }, diff --git a/src/rulesets/oas/functions/__tests__/oasOpSecurityDefined.test.ts b/src/rulesets/oas/functions/__tests__/oasOpSecurityDefined.test.ts index 27a32d3e3..d10712092 100644 --- a/src/rulesets/oas/functions/__tests__/oasOpSecurityDefined.test.ts +++ b/src/rulesets/oas/functions/__tests__/oasOpSecurityDefined.test.ts @@ -1,17 +1,17 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { RuleType, Spectral } from '../../../../index'; -import { rules as oas2Rules } from '../../../oas2/index.json'; -import { rules as oas3Rules } from '../../../oas3/index.json'; +import { rules as oasRules } from '../../../oas/index.json'; import oasOpSecurityDefined from '../oasOpSecurityDefined'; describe('oasOpSecurityDefined', () => { describe('oas2', () => { const s = new Spectral(); + s.registerFormat('oas2', () => true); s.setFunctions({ oasOpSecurityDefined }); s.setRules({ - 'oas2-operation-security-defined': Object.assign(oas2Rules['oas2-operation-security-defined'], { + 'oas2-operation-security-defined': Object.assign(oasRules['oas2-operation-security-defined'], { recommended: true, - type: RuleType[oas2Rules['oas2-operation-security-defined'].type], + type: RuleType[oasRules['oas2-operation-security-defined'].type], }), }); @@ -37,6 +37,7 @@ describe('oasOpSecurityDefined', () => { test('return errors on invalid object', async () => { const results = await s.run({ + swagger: '2.0', securityDefinitions: {}, paths: { '/path': { @@ -52,38 +53,30 @@ describe('oasOpSecurityDefined', () => { }); expect(results).toEqual([ - { + expect.objectContaining({ code: 'oas2-operation-security-defined', message: 'Operation `security` values must match a scheme defined in the `securityDefinitions` object.', path: ['paths', '/path', 'get', 'security', '0'], - range: { - end: { - character: 24, - line: 7, - }, - start: { - character: 10, - line: 6, - }, - }, severity: DiagnosticSeverity.Warning, - }, + }), ]); }); }); describe('oas3', () => { const s = new Spectral(); + s.registerFormat('oas3', () => true); s.setFunctions({ oasOpSecurityDefined }); s.setRules({ - 'oas3-operation-security-defined': Object.assign(oas3Rules['oas3-operation-security-defined'], { + 'oas3-operation-security-defined': Object.assign(oasRules['oas3-operation-security-defined'], { recommended: true, - type: RuleType[oas3Rules['oas3-operation-security-defined'].type], + type: RuleType[oasRules['oas3-operation-security-defined'].type], }), }); test('validate a correct object (just in body)', async () => { const results = await s.run({ + openapi: '3.0.2', components: { securitySchemes: { apikey: {}, @@ -106,6 +99,7 @@ describe('oasOpSecurityDefined', () => { test('return errors on invalid object', async () => { const results = await s.run({ + openapi: '3.0.2', components: {}, paths: { '/path': { @@ -121,23 +115,13 @@ describe('oasOpSecurityDefined', () => { }); expect(results).toEqual([ - { + expect.objectContaining({ code: 'oas3-operation-security-defined', message: 'Operation `security` values must match a scheme defined in the `components.securitySchemes` object.', path: ['paths', '/path', 'get', 'security', '0'], - range: { - end: { - character: 24, - line: 7, - }, - start: { - character: 10, - line: 6, - }, - }, severity: DiagnosticSeverity.Warning, - }, + }), ]); }); }); diff --git a/src/rulesets/oas/functions/__tests__/oasTagDefined.test.ts b/src/rulesets/oas/functions/__tests__/oasTagDefined.test.ts new file mode 100644 index 000000000..58d161827 --- /dev/null +++ b/src/rulesets/oas/functions/__tests__/oasTagDefined.test.ts @@ -0,0 +1,153 @@ +import { RuleType, Spectral } from '../../../../index'; + +import { DiagnosticSeverity } from '@stoplight/types'; +import { rules } from '../../index.json'; +import oasTagDefined from '../oasTagDefined'; + +describe('oasTagDefined', () => { + const s = new Spectral(); + + s.setFunctions({ oasTagDefined }); + s.setRules({ + 'operation-tag-defined': Object.assign(rules['operation-tag-defined'], { + recommended: true, + type: RuleType[rules['operation-tag-defined'].type], + }), + }); + + test('validate a correct object', async () => { + const results = await s.run({ + tags: [ + { + name: 'tag1', + }, + { + name: 'tag2', + }, + ], + paths: { + '/path1': { + get: { + tags: ['tag1'], + }, + }, + '/path2': { + get: { + tags: ['tag2'], + }, + }, + }, + }); + expect(results.length).toEqual(0); + }); + + test('return errors on undefined tag', async () => { + const results = await s.run({ + tags: [ + { + name: 'tag1', + }, + ], + paths: { + '/path1': { + get: { + tags: ['tag2'], + }, + }, + }, + }); + + expect(results).toEqual([ + { + code: 'operation-tag-defined', + message: 'Operation tags should be defined in global tags.', + path: ['paths', '/path1', 'get', 'tags', '0'], + range: { + end: { + character: 16, + line: 10, + }, + start: { + character: 10, + line: 10, + }, + }, + severity: DiagnosticSeverity.Warning, + }, + ]); + }); + + test('return errors on undefined tags among defined tags', async () => { + const results = await s.run({ + tags: [ + { + name: 'tag1', + }, + { + name: 'tag3', + }, + ], + paths: { + '/path1': { + get: { + tags: ['tag1', 'tag2', 'tag3', 'tag4'], + }, + }, + }, + }); + + expect(results).toEqual([ + { + code: 'operation-tag-defined', + message: 'Operation tags should be defined in global tags.', + path: ['paths', '/path1', 'get', 'tags', '1'], + range: { + end: { + character: 16, + line: 14, + }, + start: { + character: 10, + line: 14, + }, + }, + severity: DiagnosticSeverity.Warning, + }, + { + code: 'operation-tag-defined', + message: 'Operation tags should be defined in global tags.', + path: ['paths', '/path1', 'get', 'tags', '3'], + range: { + end: { + character: 16, + line: 16, + }, + start: { + character: 10, + line: 16, + }, + }, + severity: DiagnosticSeverity.Warning, + }, + ]); + }); + + test('resilient to no global tags or operation tags', async () => { + const results = await s.run({ + paths: { + '/path1': { + get: { + operationId: 'id1', + }, + }, + '/path2': { + get: { + operationId: 'id2', + }, + }, + }, + }); + + expect(results.length).toEqual(0); + }); +}); diff --git a/src/rulesets/oas/functions/oasTagDefined.ts b/src/rulesets/oas/functions/oasTagDefined.ts new file mode 100644 index 000000000..a13575db5 --- /dev/null +++ b/src/rulesets/oas/functions/oasTagDefined.ts @@ -0,0 +1,36 @@ +// This function will check an API doc to verify that any tag that appears on +// an operation is also present in the global tags array. + +import { IFunction, IFunctionResult, Rule } from '../../../types'; + +export const oasTagDefined: IFunction = (targetVal, _options, functionPaths) => { + const results: IFunctionResult[] = []; + + const globalTags = (targetVal.tags || []).map(({ name }: { name: string }) => name); + + const { paths = {} } = targetVal; + + const validOperationKeys = ['get', 'head', 'post', 'put', 'patch', 'delete', 'options', 'trace']; + + for (const path in paths) { + if (Object.keys(paths[path]).length > 0) { + for (const operation in paths[path]) { + if (validOperationKeys.indexOf(operation) > -1) { + const { tags = [] } = paths[path][operation]; + tags.forEach((tag: string, index: number) => { + if (globalTags.indexOf(tag) === -1) { + results.push({ + message: 'Operation tags should be defined in global tags.', + path: ['paths', path, operation, 'tags', index], + }); + } + }); + } + } + } + } + + return results; +}; + +export default oasTagDefined; diff --git a/src/rulesets/oas/functions/refSiblings.ts b/src/rulesets/oas/functions/refSiblings.ts index 1b7e991a1..4db731c11 100644 --- a/src/rulesets/oas/functions/refSiblings.ts +++ b/src/rulesets/oas/functions/refSiblings.ts @@ -1,6 +1,6 @@ +import { hasRef } from '@stoplight/json'; import { JsonPath } from '@stoplight/types'; import { IFunction, IFunctionResult } from '../../../types'; -import { hasRef } from '../../../utils/hasRef'; // function is needed because `$..$ref` or `$..[?(@.$ref)]` are not parsed correctly // and therefore lead to infinite recursion due to the dollar sign ('$' in '$ref') diff --git a/src/rulesets/oas/index.json b/src/rulesets/oas/index.json index 4a5c40e50..557861cad 100644 --- a/src/rulesets/oas/index.json +++ b/src/rulesets/oas/index.json @@ -1,5 +1,4 @@ { - "extends": ["spectral:oas2", "spectral:oas3"], "formats": ["oas2", "oas3"], "functions": [ "oasOp2xxResponse", @@ -8,6 +7,7 @@ "oasOpParams", "oasOpSecurityDefined", "oasPathParam", + "oasTagDefined", "refSiblings" ], "rules": { @@ -24,9 +24,10 @@ "operation" ] }, - "operation-formData-consume-check": { + "oas2-operation-formData-consume-check": { "description": "Operations with an `in: formData` parameter must include `application/x-www-form-urlencoded` or `multipart/form-data` in their `consumes` property.", "recommended": true, + "formats": ["oas2"], "type": "validation", "given": "$.paths.*[?( @property === 'get' || @property === 'put' || @property === 'post' || @property === 'delete' || @property === 'options' || @property === 'head' || @property === 'patch' || @property === 'trace' )]", "then": { @@ -61,6 +62,18 @@ "operation" ] }, + "operation-tag-defined": { + "description": "Operation tags should be defined in global tags.", + "recommended": true, + "type": "validation", + "given": "$", + "then": { + "function": "oasTagDefined" + }, + "tags": [ + "operation" + ] + }, "path-params": { "description": "Path parameters should be defined and valid.", "message": "{{error}}", @@ -100,7 +113,7 @@ }, "example-value-or-externalValue": { "description": "Example should have either a `value` or `externalValue` field.", - "recommended": false, + "recommended": true, "type": "style", "given": "$..example", "then": { @@ -167,7 +180,7 @@ }, "no-eval-in-markdown": { "description": "Markdown descriptions should not contain `eval(`.", - "recommended": false, + "recommended": true, "type": "style", "given": "$..*", "then": [ @@ -227,7 +240,7 @@ }, "openapi-tags": { "description": "OpenAPI object should have non-empty `tags` array.", - "recommended": false, + "recommended": true, "type": "style", "given": "$", "then": { @@ -309,22 +322,6 @@ "operation" ] }, - "operation-summary-formatted": { - "description": "Operation `summary` should start with upper case and end with a dot.", - "recommended": false, - "type": "style", - "given": "$.paths.*[?( @property === 'get' || @property === 'put' || @property === 'post' || @property === 'delete' || @property === 'options' || @property === 'head' || @property === 'patch' || @property === 'trace' )]", - "then": { - "field": "summary", - "function": "pattern", - "functionOptions": { - "match": "^[A-Z].*\\.$" - } - }, - "tags": [ - "operation" - ] - }, "operation-tags": { "description": "Operation should have non-empty `tags` array.", "recommended": true, @@ -410,6 +407,432 @@ "then": { "function": "refSiblings" } + }, + "oas2-api-host": { + "description": "OpenAPI `host` must be present and non-empty string.", + "recommended": true, + "formats": ["oas2"], + "type": "style", + "given": "$", + "then": { + "field": "host", + "function": "truthy" + }, + "tags": [ + "api" + ] + }, + "oas2-api-schemes": { + "description": "OpenAPI host `schemes` must be present and non-empty array.", + "recommended": true, + "formats": ["oas2"], + "type": "style", + "given": "$", + "then": { + "field": "schemes", + "function": "schema", + "functionOptions": { + "schema": { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + } + } + }, + "tags": [ + "api" + ] + }, + "oas2-host-not-example": { + "description": "Host URL should not point at example.com.", + "recommended": false, + "formats": ["oas2"], + "given": "$", + "type": "style", + "then": { + "field": "host", + "function": "pattern", + "functionOptions": { + "notMatch": "example\\.com" + } + }, + "tags": [ + "api" + ] + }, + "oas2-host-trailing-slash": { + "description": "Server URL should not have a trailing slash.", + "recommended": true, + "formats": ["oas2"], + "given": "$", + "type": "style", + "then": { + "field": "host", + "function": "pattern", + "functionOptions": { + "notMatch": "/$" + } + }, + "tags": [ + "api" + ] + }, + "oas2-parameter-description": { + "description": "Parameter objects should have a `description`.", + "recommended": false, + "formats": ["oas2"], + "given": "$..[?(@parentProperty === 'parameters' && @.in)]", + "type": "style", + "then": { + "field": "description", + "function": "truthy" + }, + "tags": [ + "parameters" + ] + }, + "oas2-operation-security-defined": { + "description": "Operation `security` values must match a scheme defined in the `securityDefinitions` object.", + "recommended": true, + "formats": ["oas2"], + "type": "validation", + "given": "$", + "then": { + "function": "oasOpSecurityDefined", + "functionOptions": { + "schemesPath": [ + "securityDefinitions" + ] + } + }, + "tags": [ + "operation" + ] + }, + "oas2-valid-parameter-example": { + "description": "Examples must be valid against their defined schema.", + "message": "{{error}}", + "recommended": true, + "formats": ["oas2"], + "severity": 0, + "type": "validation", + "given": "$..parameters..[?(@.in == 'body')]..[?(@property !== 'properties' && @.example && ( @.type || @.format || @.$ref ))]", + "then": { + "function": "schemaPath", + "functionOptions": { + "field": "example", + "schemaPath": "$" + } + } + }, + "oas2-valid-definition-example": { + "description": "Examples must be valid against their defined schema.", + "message": "{{error}}", + "recommended": true, + "formats": ["oas2"], + "severity": 0, + "type": "validation", + "given": "$..definitions..[?(@property !== 'properties' && @.example && (@.type || @.format || @.$ref))]", + "then": { + "function": "schemaPath", + "functionOptions": { + "field": "example", + "schemaPath": "$" + } + } + }, + "oas2-anyOf": { + "description": "OpenAPI v3 keyword `anyOf` detected in OpenAPI v2 document.", + "message": "anyOf is not available in OpenAPI v2, it was added in OpenAPI v3", + "recommended": true, + "formats": ["oas2"], + "type": "validation", + "given": "$..[?(@.anyOf)]", + "then": { + "field": "anyOf", + "function": "undefined" + }, + "tags": [ + "schema" + ] + }, + "oas2-oneOf": { + "description": "OpenAPI v3 keyword `oneOf` detected in OpenAPI v2 document.", + "message": "oneOf is not available in OpenAPI v2, it was added in OpenAPI v3", + "recommended": true, + "formats": ["oas2"], + "type": "validation", + "given": "$..[?(@.oneOf)]", + "then": { + "field": "oneOf", + "function": "undefined" + }, + "tags": [ + "schema" + ] + }, + "oas2-schema": { + "description": "Validate structure of OpenAPI v2 specification.", + "message": "{{error}}", + "recommended": true, + "formats": ["oas2"], + "severity": 0, + "type": "validation", + "given": "$", + "then": { + "function": "schema", + "functionOptions": { + "schema": { + "$ref": "./schemas/schema.oas2.json" + } + } + }, + "tags": [ + "schema" + ] + }, + "oas2-unused-definition": { + "description": "Potentially unused definition has been detected.", + "recommended": true, + "resolved": false, + "formats": ["oas2"], + "type": "style", + "given": "$.definitions", + "then": { + "function": "unreferencedReusableObject", + "functionOptions": { + "reusableObjectsLocation": "#/definitions" + } + } + }, + "oas3-api-servers": { + "description": "OpenAPI `servers` must be present and non-empty array.", + "recommended": true, + "formats": ["oas3"], + "type": "style", + "given": "$", + "then": { + "field": "servers", + "function": "schema", + "functionOptions": { + "schema": { + "items": { + "type": "object" + }, + "minItems": 1, + "type": "array" + } + } + }, + "tags": [ + "api" + ] + }, + "oas3-operation-security-defined": { + "description": "Operation `security` values must match a scheme defined in the `components.securitySchemes` object.", + "recommended": true, + "formats": ["oas3"], + "type": "validation", + "given": "$", + "then": { + "function": "oasOpSecurityDefined", + "functionOptions": { + "schemesPath": [ + "components", + "securitySchemes" + ] + } + }, + "tags": [ + "operation" + ] + }, + "oas3-parameter-description": { + "description": "Parameter objects should have a `description`.", + "recommended": false, + "formats": ["oas3"], + "type": "style", + "given": "$..[?(@parentProperty !== 'links' && @.parameters)]['parameters'].[?(@.in)]", + "then": { + "field": "description", + "function": "truthy" + }, + "tags": [ + "parameters" + ] + }, + "oas3-server-not-example.com": { + "description": "Server URL should not point at example.com.", + "recommended": false, + "formats": ["oas3"], + "type": "style", + "given": "$.servers[*].url", + "then": { + "function": "pattern", + "functionOptions": { + "notMatch": "example\\.com" + } + } + }, + "oas3-server-trailing-slash": { + "description": "Server URL should not have a trailing slash.", + "recommended": true, + "formats": ["oas3"], + "type": "style", + "given": "$.servers[*].url", + "then": { + "function": "pattern", + "functionOptions": { + "notMatch": "/$" + } + } + }, + "oas3-valid-oas-parameter-example": { + "description": "Examples must be valid against their defined schema.", + "message": "{{error}}", + "recommended": true, + "severity": 0, + "type": "validation", + "given": "$..parameters..[?(@.example && @.schema)]", + "then": { + "function": "schemaPath", + "functionOptions": { + "field": "example", + "schemaPath": "$.schema" + } + } + }, + "oas3-valid-oas-header-example": { + "description": "Examples must be valid against their defined schema.", + "message": "{{error}}", + "recommended": true, + "severity": 0, + "formats": ["oas3"], + "type": "validation", + "given": "$..headers..[?(@.example && @.schema)]", + "then": { + "function": "schemaPath", + "functionOptions": { + "field": "example", + "schemaPath": "$.schema" + } + } + }, + "oas3-valid-oas-content-example": { + "description": "Examples must be valid against their defined schema.", + "message": "{{error}}", + "recommended": true, + "severity": 0, + "formats": ["oas3"], + "type": "validation", + "given": "$..content..[?(@.example && @.schema)]", + "then": { + "function": "schemaPath", + "functionOptions": { + "field": "example", + "schemaPath": "$.schema" + } + } + }, + "oas3-valid-parameter-schema-example": { + "description": "Examples must be valid against their defined schema.", + "message": "{{error}}", + "recommended": true, + "severity": 0, + "formats": ["oas3"], + "type": "validation", + "given": "$..parameters..[?(@property !== 'properties' && @.example && (@.type || @.format || @.$ref))]", + "then": { + "function": "schemaPath", + "functionOptions": { + "field": "example", + "schemaPath": "$" + } + } + }, + "oas3-valid-header-schema-example": { + "description": "Examples must be valid against their defined schema.", + "message": "{{error}}", + "recommended": true, + "severity": 0, + "formats": ["oas3"], + "type": "validation", + "given": "$..headers..[?(@property !== 'properties' && @.example && (@.type || @.format || @.$ref))]", + "then": { + "function": "schemaPath", + "functionOptions": { + "field": "example", + "schemaPath": "$" + } + } + }, + "oas3-valid-schema-example": { + "description": "Examples must be valid against their defined schema.", + "message": "{{error}}", + "severity": 0, + "formats": ["oas3"], + "recommended": true, + "type": "validation", + "given": "$.components.schemas..[?(@property !== 'properties' && @.example && (@.type || @.format || @.$ref))]", + "then": { + "function": "schemaPath", + "functionOptions": { + "field": "example", + "schemaPath": "$" + } + } + }, + "oas3-valid-content-schema-example": { + "description": "Examples must be valid against their defined schema.", + "message": "{{error}}", + "severity": 0, + "formats": ["oas3"], + "recommended": true, + "type": "validation", + "given": "$..content..[?(@property !== 'properties' && @.example && (@.type || @.format || @.$ref))]", + "then": { + "function": "schemaPath", + "functionOptions": { + "field": "example", + "schemaPath": "$" + } + } + }, + "oas3-schema": { + "description": "Validate structure of OpenAPI v3 specification.", + "message": "{{error}}", + "severity": 0, + "formats": ["oas3"], + "recommended": true, + "type": "validation", + "given": "$", + "then": { + "function": "schema", + "functionOptions": { + "schema": { + "$ref": "./schemas/schema.oas3.json" + } + } + }, + "tags": [ + "schema" + ] + }, + "oas3-unused-components-schema": { + "description": "Potentially unused components schema has been detected.", + "recommended": true, + "formats": ["oas3"], + "type": "style", + "resolved": false, + "given": "$.components.schemas", + "then": { + "function": "unreferencedReusableObject", + "functionOptions": { + "reusableObjectsLocation": "#/components/schemas" + } + } } } } diff --git a/src/rulesets/oas/index.ts b/src/rulesets/oas/index.ts deleted file mode 100644 index f2cd16fa6..000000000 --- a/src/rulesets/oas/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { FunctionCollection } from '../../types'; -import { readRuleset } from '../reader'; - -export const commonOasFunctions = (): FunctionCollection => { - console.warn('This is deprecated. Use loadRuleset method instead'); - return { - oasPathParam: require('./functions/oasPathParam').oasPathParam, - oasOp2xxResponse: require('./functions/oasOp2xxResponse').oasOp2xxResponse, - oasOpSecurityDefined: require('./functions/oasOpSecurityDefined').oasOpSecurityDefined, // used in oas2/oas3 differently see their rulesets for details - oasOpIdUnique: require('./functions/oasOpIdUnique').oasOpIdUnique, - oasOpFormDataConsumeCheck: require('./functions/oasOpFormDataConsumeCheck').oasOpFormDataConsumeCheck, - oasOpParams: require('./functions/oasOpParams').oasOpParams, - refSiblings: require('./functions/refSiblings').refSiblings, - }; -}; - -export const rules = async () => { - console.warn('This is deprecated. Use loadRuleset method instead'); - return (await readRuleset(require.resolve('./index.json'))).rules; -}; diff --git a/src/rulesets/oas2/schemas/main.json b/src/rulesets/oas/schemas/schema.oas2.json similarity index 100% rename from src/rulesets/oas2/schemas/main.json rename to src/rulesets/oas/schemas/schema.oas2.json diff --git a/src/rulesets/oas3/schemas/main.json b/src/rulesets/oas/schemas/schema.oas3.json similarity index 100% rename from src/rulesets/oas3/schemas/main.json rename to src/rulesets/oas/schemas/schema.oas3.json diff --git a/src/rulesets/oas2/__tests__/model-description.ts b/src/rulesets/oas2/__tests__/model-description.ts deleted file mode 100644 index 5ee2de526..000000000 --- a/src/rulesets/oas2/__tests__/model-description.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { DiagnosticSeverity } from '@stoplight/types'; -import { RuleType, Spectral } from '../../../spectral'; -import * as ruleset from '../index.json'; - -describe('model-description', () => { - const s = new Spectral(); - s.setRules({ - 'model-description': Object.assign(ruleset.rules['model-description'], { - recommended: true, - type: RuleType[ruleset.rules['model-description'].type], - }), - }); - - test('validate a correct object', async () => { - const results = await s.run({ - swagger: '2.0', - paths: {}, - host: 'stoplight.io', - definitions: { - user: { - description: 'this describes the user model', - }, - }, - }); - expect(results.length).toEqual(0); - }); - - test('return errors if a definition is missing description', async () => { - const results = await s.run({ - swagger: '2.0', - paths: {}, - host: 'stoplight.io', - definitions: { user: {} }, - }); - expect(results).toEqual([ - { - code: 'model-description', - message: 'Definition `description` must be present and non-empty string.', - path: ['definitions', 'user'], - range: { - end: { - character: 14, - line: 5, - }, - start: { - character: 11, - line: 5, - }, - }, - severity: DiagnosticSeverity.Warning, - }, - ]); - }); -}); diff --git a/src/rulesets/oas2/index.json b/src/rulesets/oas2/index.json deleted file mode 100644 index 3c5d2f11e..000000000 --- a/src/rulesets/oas2/index.json +++ /dev/null @@ -1,184 +0,0 @@ -{ - "extends": ["spectral:oas"], - "formats": ["oas2"], - "rules": { - "api-host": { - "description": "OpenAPI `host` must be present and non-empty string.", - "recommended": true, - "type": "style", - "given": "$", - "then": { - "field": "host", - "function": "truthy" - }, - "tags": [ - "api" - ] - }, - "api-schemes": { - "description": "OpenAPI host `schemes` must be present and non-empty array.", - "recommended": true, - "type": "style", - "given": "$", - "then": { - "field": "schemes", - "function": "schema", - "functionOptions": { - "schema": { - "items": { - "type": "string" - }, - "minItems": 1, - "type": "array" - } - } - }, - "tags": [ - "api" - ] - }, - "host-not-example": { - "description": "Server URL should not point at `example.com`.", - "recommended": false, - "type": "style", - "given": "$", - "then": { - "field": "host", - "function": "pattern", - "functionOptions": { - "notMatch": "example\\.com" - } - } - }, - "host-trailing-slash": { - "description": "Server URL should not have a trailing slash.", - "recommended": true, - "type": "style", - "given": "$", - "then": { - "field": "host", - "function": "pattern", - "functionOptions": { - "notMatch": "/$" - } - } - }, - "model-description": { - "description": "Definition `description` must be present and non-empty string.", - "recommended": false, - "type": "style", - "given": "$..definitions[*]", - "then": { - "field": "description", - "function": "truthy" - } - }, - "oas2-parameter-description": { - "description": "Parameter objects should have a `description`.", - "recommended": false, - "type": "style", - "given": "$..[?(@parentProperty === 'parameters' && @.in)]", - "then": { - "field": "description", - "function": "truthy" - }, - "tags": [ - "parameters" - ] - }, - "oas2-operation-security-defined": { - "description": "Operation `security` values must match a scheme defined in the `securityDefinitions` object.", - "recommended": true, - "type": "validation", - "given": "$", - "then": { - "function": "oasOpSecurityDefined", - "functionOptions": { - "schemesPath": [ - "securityDefinitions" - ] - } - }, - "tags": [ - "operation" - ] - }, - "valid-example-in-parameters": { - "description": "Examples must be valid against their defined schema.", - "message": "{{error}}", - "recommended": true, - "severity": 0, - "type": "validation", - "given": "$..parameters..[?(@.in == 'body')]..[?(@property !== 'properties' && @.example && ( @.type || @.format || @.$ref ))]", - "then": { - "function": "schemaPath", - "functionOptions": { - "field": "example", - "schemaPath": "$" - } - } - }, - "valid-example-in-definitions": { - "description": "Examples must be valid against their defined schema.", - "message": "{{error}}", - "recommended": true, - "severity": 0, - "type": "validation", - "given": "$..definitions..[?(@property !== 'properties' && @.example && (@.type || @.format || @.$ref))]", - "then": { - "function": "schemaPath", - "functionOptions": { - "field": "example", - "schemaPath": "$" - } - } - }, - "oas2-anyOf": { - "description": "OpenAPI v3 keyword `anyOf` detected in OpenAPI v2 document.", - "message": "anyOf is not available in OpenAPI v2, it was added in OpenAPI v3", - "recommended": true, - "type": "validation", - "given": "$..[?(@.anyOf)]", - "then": { - "field": "anyOf", - "function": "undefined" - }, - "tags": [ - "schema" - ] - }, - "oas2-oneOf": { - "description": "OpenAPI v3 keyword `oneOf` detected in OpenAPI v2 document.", - "message": "oneOf is not available in OpenAPI v2, it was added in OpenAPI v3", - "recommended": true, - "type": "validation", - "given": "$..[?(@.oneOf)]", - "then": { - "field": "oneOf", - "function": "undefined" - }, - "tags": [ - "schema" - ] - }, - "oas2-schema": { - "description": "Validate structure of OpenAPI v2 specification.", - "message": "{{error}}", - "type": "validation", - "severity": 0, - "recommended": true, - "given": "$", - "then": { - "function": "schema", - "functionOptions": { - "schema": { - "$ref": "./schemas/main.json" - } - } - }, - "tags": [ - "schema" - ] - } - } -} diff --git a/src/rulesets/oas2/index.ts b/src/rulesets/oas2/index.ts deleted file mode 100644 index 63c90f877..000000000 --- a/src/rulesets/oas2/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { readRuleset } from '../reader'; - -export { commonOasFunctions as oas2Functions } from '../oas'; - -export const rules = async () => { - console.warn('This is deprecated. Use loadRuleset method instead'); - return (await readRuleset(require.resolve('./index.json'))).rules; -}; diff --git a/src/rulesets/oas3/__tests__/model-description.ts b/src/rulesets/oas3/__tests__/model-description.ts deleted file mode 100644 index 629c5de8c..000000000 --- a/src/rulesets/oas3/__tests__/model-description.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { DiagnosticSeverity } from '@stoplight/types'; -import { RuleType, Spectral } from '../../../spectral'; -import * as ruleset from '../index.json'; - -describe('model-description', () => { - const s = new Spectral(); - s.setRules({ - 'model-description': Object.assign(ruleset.rules['model-description'], { - recommended: true, - type: RuleType[ruleset.rules['model-description'].type], - }), - }); - - test('validate a correct object', async () => { - const results = await s.run({ - openapi: '3.0.0', - paths: {}, - components: { - schemas: { - user: { - description: 'this describes the user model', - }, - }, - }, - }); - expect(results.length).toEqual(0); - }); - - test('return errors if a definition is missing description', async () => { - const results = await s.run({ - openapi: '3.0.0', - paths: {}, - components: { - schemas: { - user: {}, - }, - }, - }); - expect(results).toEqual([ - { - code: 'model-description', - message: 'Model `description` must be present and non-empty string.', - path: ['components', 'schemas', 'user'], - range: { - end: { - character: 16, - line: 5, - }, - start: { - character: 13, - line: 5, - }, - }, - severity: DiagnosticSeverity.Warning, - }, - ]); - }); -}); diff --git a/src/rulesets/oas3/index.json b/src/rulesets/oas3/index.json deleted file mode 100644 index fa1e6f85a..000000000 --- a/src/rulesets/oas3/index.json +++ /dev/null @@ -1,219 +0,0 @@ -{ - "extends": ["spectral:oas"], - "formats": ["oas3"], - "rules": { - "api-servers": { - "description": "OpenAPI `servers` must be present and non-empty array.", - "recommended": true, - "type": "style", - "given": "$", - "then": { - "field": "servers", - "function": "schema", - "functionOptions": { - "schema": { - "items": { - "type": "object" - }, - "minItems": 1, - "type": "array" - } - } - }, - "tags": [ - "api" - ] - }, - "model-description": { - "description": "Model `description` must be present and non-empty string.", - "recommended": false, - "type": "style", - "given": "$.components.schemas[*]", - "then": { - "field": "description", - "function": "truthy" - } - }, - "oas3-operation-security-defined": { - "description": "Operation `security` values must match a scheme defined in the `components.securitySchemes` object.", - "recommended": true, - "type": "validation", - "given": "$", - "then": { - "function": "oasOpSecurityDefined", - "functionOptions": { - "schemesPath": [ - "components", - "securitySchemes" - ] - } - }, - "tags": [ - "operation" - ] - }, - "oas3-parameter-description": { - "description": "Parameter objects should have a `description`.", - "recommended": false, - "type": "style", - "given": "$..[?(@parentProperty !== 'links' && @.parameters)]['parameters'].[?(@.in)]", - "then": { - "field": "description", - "function": "truthy" - }, - "tags": [ - "parameters" - ] - }, - "server-not-example.com": { - "description": "Server URL should not point at `example.com`.", - "recommended": false, - "type": "style", - "given": "$.servers[*]", - "then": { - "field": "url", - "function": "pattern", - "functionOptions": { - "notMatch": "example\\.com" - } - } - }, - "server-trailing-slash": { - "description": "Server URL should not have a trailing slash.", - "recommended": true, - "type": "style", - "given": "$.servers[*]", - "then": { - "field": "url", - "function": "pattern", - "functionOptions": { - "notMatch": "/$" - } - } - }, - "valid-oas-example-in-parameters": { - "description": "Examples must be valid against their defined schema.", - "message": "{{error}}", - "recommended": true, - "severity": 0, - "type": "validation", - "given": "$..parameters..[?(@.example && @.schema)]", - "then": { - "function": "schemaPath", - "functionOptions": { - "field": "example", - "schemaPath": "$.schema" - } - } - }, - "valid-oas-example-in-headers": { - "description": "Examples must be valid against their defined schema.", - "message": "{{error}}", - "recommended": true, - "severity": 0, - "type": "validation", - "given": "$..headers..[?(@.example && @.schema)]", - "then": { - "function": "schemaPath", - "functionOptions": { - "field": "example", - "schemaPath": "$.schema" - } - } - }, - "valid-oas-example-in-content": { - "description": "Examples must be valid against their defined schema.", - "message": "{{error}}", - "recommended": true, - "severity": 0, - "type": "validation", - "given": "$..content..[?(@.example && @.schema)]", - "then": { - "function": "schemaPath", - "functionOptions": { - "field": "example", - "schemaPath": "$.schema" - } - } - }, - "valid-schema-example-in-parameters": { - "summary": "Examples must be valid against their defined schema.", - "message": "{{error}}", - "recommended": true, - "severity": 0, - "type": "validation", - "given": "$..parameters..[?(@property !== 'properties' && @.example && (@.type || @.format || @.$ref))]", - "then": { - "function": "schemaPath", - "functionOptions": { - "field": "example", - "schemaPath": "$" - } - } - }, - "valid-schema-example-in-headers": { - "summary": "Examples must be valid against their defined schema.", - "message": "{{error}}", - "severity": 0, - "recommended": true, - "type": "validation", - "given": "$..headers..[?(@property !== 'properties' && @.example && (@.type || @.format || @.$ref))]", - "then": { - "function": "schemaPath", - "functionOptions": { - "field": "example", - "schemaPath": "$" - } - } - }, - "valid-example-in-schemas": { - "summary": "Examples must be valid against their defined schema.", - "message": "{{error}}", - "severity": 0, - "recommended": true, - "type": "validation", - "given": "$.components.schemas..[?(@property !== 'properties' && @.example && (@.type || @.format || @.$ref))]", - "then": { - "function": "schemaPath", - "functionOptions": { - "field": "example", - "schemaPath": "$" - } - } - }, - "valid-schema-example-in-content": { - "summary": "Examples must be valid against their defined schema.", - "message": "{{error}}", - "severity": 0, - "recommended": true, - "type": "validation", - "given": "$..content..[?(@property !== 'properties' && @.example && (@.type || @.format || @.$ref))]", - "then": { - "function": "schemaPath", - "functionOptions": { - "field": "example", - "schemaPath": "$" - } - } - }, - "oas3-schema": { - "description": "Validate structure of OpenAPI v3 specification.", - "message": "{{error}}", - "type": "validation", - "severity": 0, - "recommended": true, - "given": "$", - "then": { - "function": "schema", - "functionOptions": { - "schema": { - "$ref": "./schemas/main.json" - } - } - }, - "tags": [ - "schema" - ] - } - } -} diff --git a/src/rulesets/oas3/index.ts b/src/rulesets/oas3/index.ts deleted file mode 100644 index 30f7de450..000000000 --- a/src/rulesets/oas3/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { readRuleset } from '../reader'; - -export { commonOasFunctions as oas3Functions } from '../oas'; - -export const rules = async () => { - console.warn('This is deprecated. Use loadRuleset method instead'); - return (await readRuleset(require.resolve('./index.json'))).rules; -}; diff --git a/src/rulesets/reader.ts b/src/rulesets/reader.ts index d0725c259..d424da611 100644 --- a/src/rulesets/reader.ts +++ b/src/rulesets/reader.ts @@ -3,7 +3,6 @@ import { ICache } from '@stoplight/json-ref-resolver/types'; import { join } from '@stoplight/path'; import { Optional } from '@stoplight/types'; import { parse } from '@stoplight/yaml'; -import { JSONSchema7 } from 'json-schema'; import { readFile, readParsable } from '../fs/reader'; import { httpAndFileResolver } from '../resolvers/http-and-file'; import { FileRulesetSeverity, IRuleset, RulesetFunctionCollection } from '../types/ruleset'; @@ -102,7 +101,10 @@ const createRulesetProcessor = ( } } - mergeRules(rules, ruleset.rules, severity === undefined ? 'recommended' : severity); + if (ruleset.rules !== void 0) { + mergeRules(rules, ruleset.rules, severity === undefined ? 'recommended' : severity); + } + if (Array.isArray(ruleset.formats)) { mergeFormats(rules, ruleset.formats); } @@ -117,7 +119,7 @@ const createRulesetProcessor = ( await Promise.all( rulesetFunctions.map(async fn => { const fnName = Array.isArray(fn) ? fn[0] : fn; - const fnSchema = Array.isArray(fn) ? (fn[1] as JSONSchema7) : null; + const fnSchema = Array.isArray(fn) ? fn[1] : null; try { resolvedFunctions[fnName] = { diff --git a/src/rulesets/severity.ts b/src/rulesets/severity.ts index b901c6a23..be012655e 100644 --- a/src/rulesets/severity.ts +++ b/src/rulesets/severity.ts @@ -53,7 +53,7 @@ export function getSeverityLevel( existingRule.severity !== undefined ? getDiagnosticSeverity(existingRule.severity) : DEFAULT_SEVERITY_LEVEL; if (newRule === 'recommended') { - return existingRule.recommended ? existingSeverity : -1; + return existingRule.recommended !== false ? existingSeverity : -1; } if (newRule === 'all') { diff --git a/src/rulesets/validation.ts b/src/rulesets/validation.ts index 4aa3ceefa..06c129eb5 100644 --- a/src/rulesets/validation.ts +++ b/src/rulesets/validation.ts @@ -1,11 +1,11 @@ -import { JSONSchema7 } from 'json-schema'; import { FileRule, IRulesetFile } from '../types/ruleset'; import { ErrorObject } from 'ajv'; const AJV = require('ajv'); import * as ruleSchema from '../meta/rule.schema.json'; import * as rulesetSchema from '../meta/ruleset.schema.json'; -import { IFunction, IFunctionPaths, IFunctionValues, Rule } from '../types'; +import { IFunction, IFunctionPaths, IFunctionValues, JSONSchema, Rule } from '../types'; +import { isObject } from '../utils'; const ajv = new AJV({ allErrors: true, jsonPointers: true }); const validate = ajv.addSchema(ruleSchema).compile(rulesetSchema); @@ -23,12 +23,12 @@ export class ValidationError extends AJV.ValidationError { } export function assertValidRuleset(ruleset: unknown): IRulesetFile { - if (ruleset === null || typeof ruleset !== 'object') { + if (!isObject(ruleset)) { throw new Error('Provided ruleset is not an object'); } - if (!('rules' in ruleset!)) { - throw new Error('Ruleset must have rules property'); + if (!('rules' in ruleset) && !('extends' in ruleset)) { + throw new Error('Ruleset must have rules or extends property'); } if (!validate(ruleset)) { @@ -42,7 +42,7 @@ export function isValidRule(rule: FileRule): rule is Rule { return typeof rule === 'object' && rule !== null && !Array.isArray(rule) && ('given' in rule || 'then' in rule); } -export function decorateIFunctionWithSchemaValidation(fn: IFunction, schema: JSONSchema7) { +export function decorateIFunctionWithSchemaValidation(fn: IFunction, schema: JSONSchema) { return (data: unknown, opts: unknown, ...args: [IFunctionPaths, IFunctionValues]) => { if (!ajv.validate(schema, opts)) { throw new ValidationError(ajv.errors); diff --git a/src/runner.ts b/src/runner.ts index f3d64e02d..bc9d658eb 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -1,8 +1,7 @@ -import { Resolved } from './resolved'; - const { JSONPath } = require('jsonpath-plus'); import { lintNode } from './linter'; +import { Resolved } from './resolved'; import { getDiagnosticSeverity } from './rulesets/severity'; import { FunctionCollection, IGivenNode, IRule, IRuleResult, IRunRule, RunRuleCollection } from './types'; import { hasIntersectingElement } from './utils/'; @@ -14,7 +13,7 @@ export const runRules = ( rules: RunRuleCollection, functions: FunctionCollection, ): IRuleResult[] => { - let results: IRuleResult[] = []; + const results: IRuleResult[] = []; for (const name in rules) { if (!rules.hasOwnProperty(name)) continue; @@ -26,15 +25,16 @@ export const runRules = ( rule.formats !== void 0 && (resolved.formats === null || (resolved.formats !== void 0 && !hasIntersectingElement(rule.formats, resolved.formats))) - ) + ) { continue; + } if (!isRuleEnabled(rule)) { continue; } try { - results = results.concat(runRule(resolved, rule, functions)); + results.push(...runRule(resolved, rule, functions)); } catch (e) { console.error(`Unable to run rule '${name}':\n${e}`); } @@ -47,48 +47,66 @@ const runRule = (resolved: Resolved, rule: IRunRule, functions: FunctionCollecti const target = rule.resolved === false ? resolved.unresolved : resolved.resolved; const results: IRuleResult[] = []; - const nodes: IGivenNode[] = []; - // don't have to spend time running jsonpath if given is $ - can just use the root object - if (rule.given && rule.given !== '$') { - try { + for (const given of Array.isArray(rule.given) ? rule.given : [rule.given]) { + // don't have to spend time running jsonpath if given is $ - can just use the root object + if (given === '$') { + lint( + { + path: ['$'], + value: target, + }, + resolved, + rule, + functions, + results, + ); + } else { JSONPath({ - path: rule.given, + path: given, json: target, resultType: 'all', callback: (result: any) => { - nodes.push({ - path: JSONPath.toPathArray(result.path), - value: result.value, - }); + lint( + { + path: JSONPath.toPathArray(result.path), + value: result.value, + }, + resolved, + rule, + functions, + results, + ); }, }); - } catch (e) { - console.error(e); } - } else { - nodes.push({ - path: ['$'], - value: target, - }); } - for (const node of nodes) { - try { - const thens = Array.isArray(rule.then) ? rule.then : [rule.then]; - for (const then of thens) { - const func = functions[then.function]; - if (!func) { - console.warn(`Function ${then.function} not found. Called by rule ${rule.name}.`); - continue; - } - - results.push(...lintNode(node, rule, then, func, resolved)); + return results; +}; + +function lint( + node: IGivenNode, + resolved: Resolved, + rule: IRunRule, + functions: FunctionCollection, + results: IRuleResult[], +): void { + try { + for (const then of Array.isArray(rule.then) ? rule.then : [rule.then]) { + const func = functions[then.function]; + if (!func) { + console.warn(`Function ${then.function} not found. Called by rule ${rule.name}.`); + continue; + } + + const validationResults = lintNode(node, rule, then, func, resolved); + + if (validationResults.length > 0) { + results.push(...validationResults); } - } catch (e) { - console.warn(`Encountered error when running rule '${rule.name}' on node at path '${node.path}':\n${e}`); } + } catch (e) { + console.warn(`Encountered error when running rule '${rule.name}' on node at path '${node.path}':\n${e}`); } - - return results; -}; +} diff --git a/src/spectral.ts b/src/spectral.ts index 3c4fc545b..bdff90447 100644 --- a/src/spectral.ts +++ b/src/spectral.ts @@ -1,24 +1,15 @@ -import { - getLocationForJsonPath as getLocationForJsonPathJSON, - JsonParserResult, - parseWithPointers as parseJSONWithPointers, - safeStringify, -} from '@stoplight/json'; +import { getLocationForJsonPath as getLocationForJsonPathJson, JsonParserResult, safeStringify } from '@stoplight/json'; import { Resolver } from '@stoplight/json-ref-resolver'; import { ICache, IUriParser } from '@stoplight/json-ref-resolver/types'; -import { extname } from '@stoplight/path'; -import { Dictionary } from '@stoplight/types'; -import { - getLocationForJsonPath as getLocationForJsonPathYAML, - parseWithPointers as parseYAMLWithPointers, - YamlParserResult, -} from '@stoplight/yaml'; -import { merge, set } from 'lodash'; - -import { IDiagnostic } from '@stoplight/types/dist'; -import deprecated from 'deprecated-decorator'; +import { extname, normalize } from '@stoplight/path'; +import { DiagnosticSeverity, Dictionary, IDiagnostic, Optional } from '@stoplight/types'; +import { getLocationForJsonPath as getLocationForJsonPathYaml, YamlParserResult } from '@stoplight/yaml'; +import { memoize, merge } from 'lodash'; + +import { STATIC_ASSETS } from './assets'; import { formatParserDiagnostics, formatResolverErrors } from './error-messages'; import { functions as defaultFunctions } from './functions'; +import { parseJson, parseYaml } from './parsers'; import { Resolved } from './resolved'; import { readRuleset } from './rulesets'; import { compileExportedFunction } from './rulesets/evaluators'; @@ -40,37 +31,43 @@ import { RunRuleCollection, } from './types'; import { IRuleset } from './types/ruleset'; +import { ComputeFingerprintFunc, defaultComputeResultFingerprint, empty, prepareResults } from './utils'; + +memoize.Cache = WeakMap; export * from './types'; export class Spectral { - private readonly _resolver: IResolver; - private readonly _parsedMap: IParseMap; - private static readonly _parsedCache = new WeakMap(); public functions: FunctionCollection = { ...defaultFunctions }; public rules: RunRuleCollection = {}; - public formats: RegisteredFormats; + private readonly _computeFingerprint: ComputeFingerprintFunc; + private readonly _resolver: IResolver; + private readonly _parsedRefs: Dictionary; + private static readonly _parsedCache = new WeakMap>(); + constructor(opts?: IConstructorOpts) { + this._computeFingerprint = memoize(opts?.computeFingerprint || defaultComputeResultFingerprint); this._resolver = opts && opts.resolver ? opts.resolver : new Resolver(); this.formats = {}; const cacheKey = this._resolver instanceof Resolver ? this._resolver.uriCache : this._resolver; - const _parsedMap = Spectral._parsedCache.get(cacheKey); - if (_parsedMap) { - this._parsedMap = _parsedMap; + const _parsedRefs = Spectral._parsedCache.get(cacheKey); + if (_parsedRefs) { + this._parsedRefs = _parsedRefs; } else { - this._parsedMap = { - refs: {}, - parsed: {}, - pointers: {}, - }; + this._parsedRefs = {}; - Spectral._parsedCache.set(cacheKey, this._parsedMap); + Spectral._parsedCache.set(cacheKey, this._parsedRefs); } } + public static registerStaticAssets(assets: Dictionary) { + empty(STATIC_ASSETS); + Object.assign(STATIC_ASSETS, assets); + } + public async runWithResolved( target: IParsedResult | object | string, opts: IRunOpts = {}, @@ -80,11 +77,10 @@ export class Spectral { let parsedResult: IParsedResult | IParsedResult>; if (!isParsedResult(target)) { parsedResult = { - parsed: parseYAMLWithPointers(typeof target === 'string' ? target : safeStringify(target, undefined, 2), { - ignoreDuplicateKeys: false, - mergeKeys: true, - }), - getLocationForJsonPath: getLocationForJsonPathYAML, + parsed: parseYaml(typeof target === 'string' ? target : safeStringify(target, undefined, 2)), + getLocationForJsonPath: getLocationForJsonPathYaml, + // we need to normalize the path in case path with forward slashes is given + source: opts.resolve?.documentUri && normalize(opts.resolve.documentUri), }; } else { parsedResult = target; @@ -101,24 +97,30 @@ export class Spectral { baseUri: documentUri, parseResolveResult: this._parseResolveResult(refDiagnostics), }), - this._parsedMap, + this._parsedRefs, ); + const validationResults = [...refDiagnostics, ...results, ...formatResolverErrors(resolved)]; + if (resolved.formats === void 0) { - const foundFormats = Object.keys(this.formats).filter(format => this.formats[format](resolved.resolved)); - resolved.formats = foundFormats.length === 0 ? null : foundFormats; + const registeredFormats = Object.keys(this.formats); + const foundFormats = registeredFormats.filter(format => this.formats[format](resolved.resolved)); + if (foundFormats.length === 0 && opts.ignoreUnknownFormat !== true) { + resolved.formats = null; + if (registeredFormats.length > 0) { + validationResults.push(this._generateUnrecognizedFormatError(parsedResult)); + } + } else { + resolved.formats = foundFormats; + } } - const validationResults = [ - ...refDiagnostics, - ...results, - ...formatResolverErrors(resolved), - ...runRules(resolved, this.rules, this.functions), - ]; - return { resolved: resolved.resolved, - results: validationResults, + results: prepareResults( + [...validationResults, ...runRules(resolved, this.rules, this.functions)], + this._computeFingerprint, + ), }; } @@ -126,39 +128,15 @@ export class Spectral { return (await this.runWithResolved(target, opts)).results; } - @deprecated('loadRuleset', '4.1') - public addFunctions(functions: FunctionCollection) { - this._addFunctions(functions); - } - - public _addFunctions(functions: FunctionCollection) { - Object.assign(this.functions, functions); - } - public setFunctions(functions: FunctionCollection) { - for (const key in this.functions) { - if (!Object.hasOwnProperty.call(this.functions, key)) continue; - delete this.functions[key]; - } - - this._addFunctions({ ...defaultFunctions, ...functions }); - } + empty(this.functions); - @deprecated('loadRuleset', '4.1') - public addRules(rules: RuleCollection) { - this._addRules(rules); + Object.assign(this.functions, { ...defaultFunctions, ...functions }); } public setRules(rules: RuleCollection) { - for (const key in this.rules) { - if (!Object.hasOwnProperty.call(this.rules, key)) continue; - delete this.rules[key]; - } + empty(this.rules); - this._addRules({ ...rules }); - } - - private _addRules(rules: RuleCollection) { for (const name in rules) { if (!rules.hasOwnProperty(name)) continue; const rule = rules[name]; @@ -216,63 +194,55 @@ export class Spectral { this.formats[format] = fn; } - private _processExternalRef(parsedResult: IParsedResult, opts: IUriParser) { - const ref = opts.targetAuthority.toString(); - this._parsedMap.parsed[ref] = parsedResult; - this._parsedMap.pointers[ref] = opts.parentPath; - const parentRef = opts.parentAuthority.toString(); - - set( - this._parsedMap.refs, - [...(this._parsedMap.pointers[parentRef] ? this._parsedMap.pointers[parentRef] : []), ...opts.parentPath], - Object.defineProperty({}, REF_METADATA, { - enumerable: false, - writable: false, - value: { - ref, - root: opts.fragment.split('/').slice(1), - }, - }), - ); - } - private _parseResolveResult = (refDiagnostics: IDiagnostic[]) => async (resolveOpts: IUriParser) => { - const ref = resolveOpts.targetAuthority.toString(); + const ref = resolveOpts.targetAuthority.href().replace(/\/$/, ''); const ext = extname(ref); const content = String(resolveOpts.result); - let parsedRefResult: - | IParsedResult> - | IParsedResult> - | undefined; + let parsedRefResult: Optional> | IParsedResult>>; if (ext === '.yml' || ext === '.yaml') { parsedRefResult = { - parsed: parseYAMLWithPointers(content, { ignoreDuplicateKeys: false }), + parsed: parseYaml(content), source: ref, - getLocationForJsonPath: getLocationForJsonPathYAML, + getLocationForJsonPath: getLocationForJsonPathYaml, }; } else if (ext === '.json') { parsedRefResult = { - parsed: parseJSONWithPointers(content, { ignoreDuplicateKeys: false }), + parsed: parseJson(content), source: ref, - getLocationForJsonPath: getLocationForJsonPathJSON, + getLocationForJsonPath: getLocationForJsonPathJson, }; } - if (parsedRefResult !== undefined) { + if (parsedRefResult !== void 0) { resolveOpts.result = parsedRefResult.parsed.data; if (parsedRefResult.parsed.diagnostics.length > 0) { refDiagnostics.push(...formatParserDiagnostics(parsedRefResult.parsed.diagnostics, parsedRefResult.source)); } - this._processExternalRef(parsedRefResult, resolveOpts); + this._parsedRefs[ref] = parsedRefResult; } return resolveOpts; }; -} -export const REF_METADATA = Symbol('external_ref_metadata'); + private _generateUnrecognizedFormatError(parsedResult: IParsedResult): IRuleResult { + return { + range: parsedResult.getLocationForJsonPath(parsedResult.parsed, [], true)?.range || { + start: { character: 0, line: 0 }, + end: { character: 0, line: 0 }, + }, + + message: `The provided document does not match any of the registered formats [${Object.keys(this.formats).join( + ', ', + )}]`, + code: 'unrecognized-format', + severity: DiagnosticSeverity.Warning, + source: parsedResult.source, + path: [], + }; + } +} export const isParsedResult = (obj: any): obj is IParsedResult => { if (!obj || typeof obj !== 'object') return false; @@ -281,9 +251,3 @@ export const isParsedResult = (obj: any): obj is IParsedResult => { return true; }; - -export interface IParseMap { - refs: Dictionary; - parsed: Dictionary; - pointers: Dictionary; -} diff --git a/src/types/config.ts b/src/types/config.ts index c6c524f1a..d584c7ec1 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -6,14 +6,19 @@ export enum OutputFormat { JSON = 'json', STYLISH = 'stylish', JUNIT = 'junit', + HTML = 'html', + TEXT = 'text', + TEAMCITY = 'teamcity', } export interface ILintConfig { encoding: string; format: OutputFormat; output?: string; + resolver?: string; ruleset?: string[]; skipRule?: string[]; + ignoreUnknownFormat: boolean; verbose?: boolean; quiet?: boolean; } diff --git a/src/types/enums.ts b/src/types/enums.ts index 8de2842d3..2b96bc23b 100644 --- a/src/types/enums.ts +++ b/src/types/enums.ts @@ -5,6 +5,7 @@ export enum RuleType { export enum RuleFunction { ALPHABETICAL = 'alphabetical', + CASING = 'casing', LENGTH = 'length', PATTERN = 'pattern', ENUM = 'enumeration', diff --git a/src/types/function.ts b/src/types/function.ts index a42300bde..31585dae6 100644 --- a/src/types/function.ts +++ b/src/types/function.ts @@ -1,4 +1,5 @@ import { JsonPath } from '@stoplight/types'; +import { Resolved } from '../resolved'; export type IFunction = ( targetValue: any, @@ -15,6 +16,7 @@ export interface IFunctionPaths { export interface IFunctionValues { original: any; given: any; + resolved: Resolved; } export interface IFunctionResult { diff --git a/src/types/rule.ts b/src/types/rule.ts index 6064de809..5e672488d 100644 --- a/src/types/rule.ts +++ b/src/types/rule.ts @@ -1,7 +1,7 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { RuleFunction, RuleType } from './enums'; -export type Rule = IRule | TruthyRule | XorRule | LengthRule | AlphaRule | PatternRule | SchemaRule; +export type Rule = IRule | TruthyRule | XorRule | LengthRule | AlphaRule | PatternRule | CasingRule | SchemaRule; export interface IRule { type?: RuleType; @@ -21,24 +21,16 @@ export interface IRule { tags?: string[]; // some rules are more important than others, recommended rules will be enabled by default + // true by default recommended?: boolean; // Filter the target down to a subset[] with a JSON path - given: string; + given: string | string[]; // If false, rule will operate on original (unresolved) data // If undefined or true, resolved data will be supplied resolved?: boolean; - when?: { - // the `path.to.prop` to field, or special `@key` value to target keys for matched `given` object - // EXAMPLE: if the target object is an oas object and given = `$..responses[*]`, then `@key` would be the response code (200, 400, etc) - field: string; - - // a regex pattern - pattern?: string; - }; - then: IThen | Array>; } @@ -88,6 +80,12 @@ export interface IRulePatternOptions { } export type PatternRule = IRule; +export interface ICasingOptions { + type: 'flat' | 'camel' | 'pascal' | 'kebab' | 'cobol' | 'snake' | 'macro'; + disallowDigits?: boolean; +} +export type CasingRule = IRule; + export interface ISchemaOptions { schema: object; } diff --git a/src/types/ruleset.ts b/src/types/ruleset.ts index 8dccf2efa..e084da8cc 100644 --- a/src/types/ruleset.ts +++ b/src/types/ruleset.ts @@ -1,8 +1,7 @@ import { Dictionary } from '@stoplight/types'; import { DiagnosticSeverity } from '@stoplight/types'; -import { JSONSchema4, JSONSchema6, JSONSchema7 } from 'json-schema'; import { HumanReadableDiagnosticSeverity, Rule } from './rule'; -import { RuleCollection } from './spectral'; +import { JSONSchema, RuleCollection } from './spectral'; export type FileRuleSeverity = DiagnosticSeverity | HumanReadableDiagnosticSeverity | boolean; export type FileRulesetSeverity = 'off' | 'recommended' | 'all'; @@ -14,7 +13,7 @@ export type FileRuleCollection = Dictionary; export interface IRulesetFunctionDefinition { code?: string; ref?: string; - schema: JSONSchema7 | null; + schema: JSONSchema | null; name: string; } @@ -28,7 +27,7 @@ export interface IRuleset { export interface IRulesetFile { extends?: Array; formats?: string[]; - rules: FileRuleCollection; + rules?: FileRuleCollection; functionsDir?: string; - functions?: Array; + functions?: Array; } diff --git a/src/types/spectral.ts b/src/types/spectral.ts index 4cbcc4e3d..e6e404990 100644 --- a/src/types/spectral.ts +++ b/src/types/spectral.ts @@ -7,7 +7,9 @@ import { IParserResult, JsonPath, } from '@stoplight/types'; +import { JSONSchema4, JSONSchema6, JSONSchema7 } from 'json-schema'; import { IFunction, IRule, Rule } from '.'; +import { ComputeFingerprintFunc } from '../utils'; export type FunctionCollection = Dictionary; export type RuleCollection = Dictionary; @@ -30,9 +32,11 @@ export type RuleDeclarationCollection = Dictionary; export interface IConstructorOpts { resolver?: IResolver; + computeFingerprint?: ComputeFingerprintFunc; } export interface IRunOpts { + ignoreUnknownFormat?: boolean; resolve?: { documentUri?: string; }; @@ -67,3 +71,5 @@ export interface IResolver { export type FormatLookup = (document: unknown) => boolean; export type RegisteredFormats = Dictionary; + +export type JSONSchema = JSONSchema4 | JSONSchema6 | JSONSchema7; diff --git a/src/utils/__tests__/__fixtures__/duplicate-validation-results.json b/src/utils/__tests__/__fixtures__/duplicate-validation-results.json new file mode 100644 index 000000000..a436eb98d --- /dev/null +++ b/src/utils/__tests__/__fixtures__/duplicate-validation-results.json @@ -0,0 +1,105 @@ +[ + { + "code": "valid-schema-example-in-content", + "message": "\"schema.example\" property type should be integer", + "path": [ + "paths", + "/resource", + "get", + "responses", + "200", + "content", + "application/json", + "schema", + "example" + ], + "severity": 0, + "source": "/home/Spectral/test-harness/scenarios/documents/invalid-schema-example/lib.yaml", + "range": { + "start": { + "line": 20, + "character": 15 + }, + "end": { + "line": 20, + "character": 19 + } + } + }, + { + "code": "valid-schema-example-in-content", + "message": "\"schema.example\" property type should be integer", + "path": [ + "paths", + "/resource", + "get", + "responses", + "200", + "content", + "application/json", + "schema", + "example" + ], + "severity": 0, + "source": "/home/Spectral/test-harness/scenarios/documents/invalid-schema-example/lib.yaml", + "range": { + "start": { + "line": 20, + "character": 15 + }, + "end": { + "line": 20, + "character": 19 + } + } + }, + { + "code": "valid-schema-example-in-content", + "message": "\"schema.example\" property type should be integer", + "path": [ + "paths", + "/resource", + "get", + "responses", + "200", + "content", + "application/json", + "schema", + "example" + ], + "severity": 0, + "source": "/home/Spectral/test-harness/scenarios/documents/invalid-schema-example/lib.yaml", + "range": { + "start": { + "line": 20, + "character": 15 + }, + "end": { + "line": 20, + "character": 19 + } + } + }, + { + "code": "valid-example-in-schemas", + "message": "\"Test.example\" property type should be integer", + "path": [ + "components", + "schemas", + "Test", + "example" + ], + "severity": 0, + "source": "/home/Spectral/test-harness/scenarios/documents/invalid-schema-example/lib.yaml", + "range": { + "start": { + "line": 20, + "character": 15 + }, + "end": { + "line": 20, + "character": 19 + } + } + } +] diff --git a/src/utils/__tests__/prepareResults.spec.ts b/src/utils/__tests__/prepareResults.spec.ts new file mode 100644 index 000000000..07960785e --- /dev/null +++ b/src/utils/__tests__/prepareResults.spec.ts @@ -0,0 +1,43 @@ +import { defaultComputeResultFingerprint, prepareResults } from '../prepareResults'; + +import * as duplicateValidationResults from './__fixtures__/duplicate-validation-results.json'; + +describe('prepareResults util', () => { + it('deduplicate exact validation results', () => { + expect(prepareResults(duplicateValidationResults, defaultComputeResultFingerprint)).toEqual([ + expect.objectContaining({ + code: 'valid-example-in-schemas', + }), + expect.objectContaining({ + code: 'valid-schema-example-in-content', + }), + ]); + }); + + it('deduplicate exact validation results with unknown source', () => { + const duplicateValidationResultsWithNoSource = duplicateValidationResults.map(result => ({ + ...result, + source: void 0, + })); + + expect(prepareResults(duplicateValidationResultsWithNoSource, defaultComputeResultFingerprint)).toEqual([ + expect.objectContaining({ + code: 'valid-example-in-schemas', + }), + expect.objectContaining({ + code: 'valid-schema-example-in-content', + }), + ]); + }); + + it('deduplicate list of only duplicates', () => { + const onlyDuplicates = [ + { ...duplicateValidationResults[0] }, + { ...duplicateValidationResults[0] }, + { ...duplicateValidationResults[0] }, + { ...duplicateValidationResults[0] }, + ]; + + expect(prepareResults(onlyDuplicates, defaultComputeResultFingerprint).length).toBe(1); + }); +}); diff --git a/src/utils/__tests__/printPath.spec.ts b/src/utils/__tests__/printPath.spec.ts new file mode 100644 index 000000000..d7e018d0a --- /dev/null +++ b/src/utils/__tests__/printPath.spec.ts @@ -0,0 +1,106 @@ +import { printPath, PrintStyle } from '../printPath'; + +describe('printPath util', () => { + describe('dot style', () => { + it('handles basic scenarios', () => { + expect(printPath([], PrintStyle.Dot)).toEqual(''); + expect(printPath(['user'], PrintStyle.Dot)).toEqual('user'); + expect(printPath(['user', 'age'], PrintStyle.Dot)).toEqual('user.age'); + }); + + it('handles numeric segments', () => { + expect(printPath(['schema', 'addresses', 0, 'street'], PrintStyle.Dot)).toEqual('schema.addresses[0].street'); + expect(printPath(['foo', '0', 1.2, 4], PrintStyle.Dot)).toEqual('foo[0][1.2][4]'); + }); + + it('handles empty strings', () => { + expect(printPath([''], PrintStyle.Dot)).toEqual("['']"); + expect(printPath(['a', '', 'test'], PrintStyle.Dot)).toEqual("a[''].test"); + }); + + it('handles whitespaces', () => { + expect(printPath(['a', ' bar ', 'test'], PrintStyle.Dot)).toEqual("a[' bar '].test"); + expect(printPath(['a', ' ', 'test'], PrintStyle.Dot)).toEqual("a[' '].test"); + }); + + it('decodes ~0 and ~1', () => { + expect(printPath(['paths', '~1pets', 'wildcard*~0'], PrintStyle.Dot)).toEqual('paths./pets.wildcard*~'); + }); + }); + + describe('pointer style', () => { + it('handles basic scenarios', () => { + expect(printPath([], PrintStyle.Pointer)).toEqual('#'); + expect(printPath(['user'], PrintStyle.Pointer)).toEqual('#/user'); + expect(printPath(['user', 'age'], PrintStyle.Pointer)).toEqual('#/user/age'); + }); + + it('handles numeric segments', () => { + expect(printPath(['schema', 'addresses', 0, 'street'], PrintStyle.Pointer)).toEqual( + '#/schema/addresses/0/street', + ); + expect(printPath(['foo', '0', 1.2, 4], PrintStyle.Pointer)).toEqual('#/foo/0/1.2/4'); + }); + + it('handles empty strings', () => { + expect(printPath([''], PrintStyle.Pointer)).toEqual('#/'); + expect(printPath(['a', '', 'test'], PrintStyle.Pointer)).toEqual('#/a//test'); + }); + + it('handles whitespaces', () => { + expect(printPath(['a', ' bar ', 'test'], PrintStyle.Pointer)).toEqual('#/a/ bar /test'); + expect(printPath(['a', ' ', 'test'], PrintStyle.Pointer)).toEqual('#/a/ /test'); + }); + + it('preserves slashes', () => { + expect(printPath(['paths', '/pets'], PrintStyle.Pointer)).toEqual('#/paths//pets'); + }); + + it('preserves tildes', () => { + expect(printPath(['paths', 'wildcard~'], PrintStyle.Pointer)).toEqual('#/paths/wildcard~'); + }); + + it('decodes ~0 and ~1', () => { + expect(printPath(['paths', '~1pets', 'wildcard*~0'], PrintStyle.Pointer)).toEqual('#/paths//pets/wildcard*~'); + }); + }); + + describe('escaped pointer style', () => { + it('handles basic scenarios', () => { + expect(printPath([], PrintStyle.EscapedPointer)).toEqual('#'); + expect(printPath(['user'], PrintStyle.EscapedPointer)).toEqual('#/user'); + expect(printPath(['user', 'age'], PrintStyle.EscapedPointer)).toEqual('#/user/age'); + }); + + it('handles numeric segments', () => { + expect(printPath(['schema', 'addresses', 0, 'street'], PrintStyle.EscapedPointer)).toEqual( + '#/schema/addresses/0/street', + ); + expect(printPath(['foo', '0', 1.2, 4], PrintStyle.EscapedPointer)).toEqual('#/foo/0/1.2/4'); + }); + + it('handles empty strings', () => { + expect(printPath([''], PrintStyle.EscapedPointer)).toEqual('#/'); + expect(printPath(['a', '', 'test'], PrintStyle.EscapedPointer)).toEqual('#/a//test'); + }); + + it('handles whitespaces', () => { + expect(printPath(['a', ' bar ', 'test'], PrintStyle.EscapedPointer)).toEqual('#/a/ bar /test'); + expect(printPath(['a', ' ', 'test'], PrintStyle.EscapedPointer)).toEqual('#/a/ /test'); + }); + + it('escapes slashes', () => { + expect(printPath(['paths', '/pets'], PrintStyle.EscapedPointer)).toEqual('#/paths/~1pets'); + }); + + it('escapes tildes', () => { + expect(printPath(['paths', 'wildcard~'], PrintStyle.EscapedPointer)).toEqual('#/paths/wildcard~0'); + }); + + it('preserves ~0 and ~1', () => { + expect(printPath(['paths', '~1pets', 'wildcard*~0'], PrintStyle.EscapedPointer)).toEqual( + '#/paths/~1pets/wildcard*~0', + ); + }); + }); +}); diff --git a/src/utils/__tests__/refs.jest.test.ts b/src/utils/__tests__/refs.jest.test.ts new file mode 100644 index 000000000..03e833a92 --- /dev/null +++ b/src/utils/__tests__/refs.jest.test.ts @@ -0,0 +1,48 @@ +import { traverseObjUntilRef } from '..'; + +describe('$ref utils', () => { + describe('traverseObjUntilRef', () => { + it('given a broken json path, throws', () => { + const obj = { + foo: { + bar: { + baz: '', + }, + }, + bar: '', + }; + + expect(traverseObjUntilRef.bind(null, obj, ['foo', 'baz'])).toThrow('Segment is not a part of the object'); + expect(traverseObjUntilRef.bind(null, obj, ['bar', 'foo'])).toThrow('Segment is not a part of the object'); + }); + + it('given a json path pointing at object with ref, returns the ref', () => { + const obj = { + x: { + $ref: 'test.json#', + }, + foo: { + bar: { + $ref: '../a.json#', + }, + }, + }; + + expect(traverseObjUntilRef(obj, ['x', 'bar'])).toEqual('test.json#'); + expect(traverseObjUntilRef(obj, ['foo', 'bar', 'baz'])).toEqual('../a.json#'); + }); + + it('given a finite json path pointing at value in project, returns null', () => { + const obj = { + x: {}, + foo: { + bar: 'test', + }, + }; + + expect(traverseObjUntilRef(obj, ['x'])).toBeNull(); + expect(traverseObjUntilRef(obj, ['foo'])).toBeNull(); + expect(traverseObjUntilRef(obj, ['foo', 'bar'])).toBeNull(); + }); + }); +}); diff --git a/src/utils/__tests__/replacer.spec.ts b/src/utils/__tests__/replacer.spec.ts new file mode 100644 index 000000000..a2754629c --- /dev/null +++ b/src/utils/__tests__/replacer.spec.ts @@ -0,0 +1,68 @@ +import { Dictionary } from '@stoplight/types'; +import { Replacer } from '../replacer'; + +describe('Replacer', () => { + it('interpolates correctly', () => { + const replacer = new Replacer>(2); + const template = 'oops... "{{property}}" is missing;error: {{error}}'; + expect( + replacer.print(template, { + property: 'description', + error: 'expected property to be truthy', + }), + ).toEqual('oops... "description" is missing;error: expected property to be truthy'); + }); + + it.each([0, false, null, undefined])('interpolates %s value correctly', value => { + const replacer = new Replacer>(2); + const template = 'Value must not equal {{value}}'; + expect( + replacer.print(template, { + value, + }), + ).toEqual(`Value must not equal ${value}`); + }); + + it('handles siblings', () => { + const replacer = new Replacer>(2); + const template = '{{error}}{{error}}{{property}}{{bar}}{{error}}{{error}}'; + expect( + replacer.print(template, { + property: 'baz', + error: 'foo', + path: '', + value: void 0, + }), + ).toEqual('foofoobazfoofoo'); + }); + + it('ignores new lines', () => { + const replacer = new Replacer>(2); + const template = '{{\ntest}}'; + expect(replacer.print(template, {})).toEqual(template); + }); + + it('strips missing keys', () => { + const replacer = new Replacer>(2); + const template = '{{foo}}missing {{bar}}:('; + expect( + replacer.print(template, { + property: 'description', + error: 'expected property to be truthy', + }), + ).toEqual('missing :('); + }); + + it('supports transformers', () => { + const replacer = new Replacer>(2); + replacer.addTransformer('dot', (id, value) => (Array.isArray(value) ? value.join('.') : String(value))); + + const template = '{{path|dot}}'; + + expect( + replacer.print(template, { + path: ['foo', 'bar', '/a'], + }), + ).toEqual('foo.bar./a'); + }); +}); diff --git a/src/utils/__tests__/sortResults.test.ts b/src/utils/__tests__/sortResults.test.ts new file mode 100644 index 000000000..00b2441fc --- /dev/null +++ b/src/utils/__tests__/sortResults.test.ts @@ -0,0 +1,206 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import { IRuleResult } from '../../types'; +import { compareResults, sortResults } from '../sortResults'; + +const results: IRuleResult[] = [ + { + code: 'code 01', + path: ['a', 'b', 'c', 'd'], + source: 'source 01', + range: { + start: { line: 1, character: 1 }, + end: { line: 99, character: 99 }, + }, + message: '99', + severity: DiagnosticSeverity.Error, // or any other level, it's irrelevant + }, + { + code: 'code 01', + path: ['a', 'b', 'c', 'd'], + source: 'source 02', + range: { + start: { line: 1, character: 1 }, + end: { line: 99, character: 99 }, + }, + message: '99', + severity: DiagnosticSeverity.Error, // or any other level, it's irrelevant + }, + { + code: 'code 01', + path: ['a', 'b', 'c', 'd'], + source: 'source 02', + range: { + start: { line: 2, character: 1 }, + end: { line: 99, character: 99 }, + }, + message: '99', + severity: DiagnosticSeverity.Error, // or any other level, it's irrelevant + }, + { + code: 'code 01', + path: ['a', 'b', 'c', 'd'], + source: 'source 02', + range: { + start: { line: 2, character: 2 }, + end: { line: 99, character: 99 }, + }, + message: '99', + severity: DiagnosticSeverity.Error, // or any other level, it's irrelevant + }, + { + code: 'code 02', + path: ['a', 'b', 'c', 'd'], + source: 'source 02', + range: { + start: { line: 2, character: 2 }, + end: { line: 99, character: 99 }, + }, + message: '99', + severity: DiagnosticSeverity.Error, // or any other level, it's irrelevant + }, + { + code: 'code 02', + path: ['a', 'b', 'c', 'e'], + source: 'source 02', + range: { + start: { line: 2, character: 2 }, + end: { line: 99, character: 99 }, + }, + message: '99', + severity: DiagnosticSeverity.Error, // or any other level, it's irrelevant + }, + { + code: 'code 02', + path: ['a', 'b', 'c', 'f'], + source: 'source 02', + range: { + start: { line: 2, character: 2 }, + end: { line: 99, character: 99 }, + }, + message: '99', + severity: DiagnosticSeverity.Error, // or any other level, it's irrelevant + }, + { + code: 'code 03', + path: ['a', 'b', 'c', 'f'], + source: 'source 02', + range: { + start: { line: 2, character: 2 }, + end: { line: 99, character: 99 }, + }, + message: '99', + severity: DiagnosticSeverity.Error, // or any other level, it's irrelevant + }, + { + code: 'code 03', + path: ['a', 'b', 'c', 'f'], + source: 'source 02', + range: { + start: { line: 2, character: 3 }, + end: { line: 99, character: 99 }, + }, + message: '99', + severity: DiagnosticSeverity.Error, // or any other level, it's irrelevant + }, + { + code: 'code 03', + path: ['a', 'b', 'c', 'f'], + source: 'source 02', + range: { + start: { line: 3, character: 3 }, + end: { line: 99, character: 99 }, + }, + message: '99', + severity: DiagnosticSeverity.Error, // or any other level, it's irrelevant + }, + { + code: 'code 03', + path: ['a', 'b', 'c', 'f'], + source: 'source 03', + range: { + start: { line: 3, character: 3 }, + end: { line: 99, character: 99 }, + }, + message: '99', + severity: DiagnosticSeverity.Error, // or any other level, it's irrelevant + }, +]; + +describe('sortResults', () => { + const shuffleBy = (arr: IRuleResult[], indices: number[]): IRuleResult[] => { + expect(indices).toHaveLength(arr.length); + + const shuffled = results + .map((v, i) => ({ ...v, pos: indices[i] })) + .sort((a, b) => a.pos - b.pos) + .map((v, i) => { + delete v.pos; + return v; + }); + + return shuffled; + }; + + test('should properly order results', () => { + const randomlySortedIndices = [5, 4, 1, 10, 8, 6, 3, 9, 2, 0, 7]; + + const shuffled = shuffleBy(results, randomlySortedIndices); + + expect(sortResults(shuffled)).toEqual(results); + }); +}); + +describe('compareResults', () => { + test('should properly order results source', () => { + const input = { + code: 'code 01', + path: ['a', 'b', 'c', 'd'], + range: { + start: { line: 1, character: 1 }, + end: { line: 99, character: 99 }, + }, + message: '99', + severity: DiagnosticSeverity.Error, // or any other level, it's irrelevant + }; + + [ + { one: undefined, another: undefined, expected: 0 }, + { one: 'a', another: undefined, expected: 1 }, + { one: undefined, another: 'a', expected: -1 }, + { one: 'a', another: 'a', expected: 0 }, + { one: 'a', another: 'b', expected: -1 }, + ].forEach(tc => { + expect(compareResults({ ...input, source: tc.one }, { ...input, source: tc.another })).toEqual(tc.expected); + }); + }); + + test('should properly order results code', () => { + const input = { + source: 'somewhere', + path: ['a', 'b', 'c', 'd'], + range: { + start: { line: 1, character: 1 }, + end: { line: 99, character: 99 }, + }, + message: '99', + severity: DiagnosticSeverity.Error, // or any other level, it's irrelevant + }; + + [ + { one: undefined, another: undefined, expected: 0 }, + { one: 'a', another: undefined, expected: 1 }, + { one: undefined, another: 'a', expected: -1 }, + { one: 'a', another: 'a', expected: 0 }, + { one: 'a', another: 'b', expected: -1 }, + { one: '2', another: '10', expected: -1 }, + { one: 1, another: undefined, expected: 1 }, + { one: undefined, another: 1, expected: -1 }, + { one: 1, another: 1, expected: 0 }, + { one: 1, another: 2, expected: -1 }, + { one: 1, another: '1', expected: 0 }, + { one: 2, another: '10', expected: -1 }, + ].forEach(tc => { + expect(compareResults({ ...input, code: tc.one }, { ...input, code: tc.another })).toEqual(tc.expected); + }); + }); +}); diff --git a/src/utils/empty.ts b/src/utils/empty.ts new file mode 100644 index 000000000..cf80a31a2 --- /dev/null +++ b/src/utils/empty.ts @@ -0,0 +1,10 @@ +import { Dictionary } from '@stoplight/types'; + +export const empty = (obj: Dictionary) => { + for (const key in obj) { + if (!Object.hasOwnProperty.call(obj, key)) continue; + delete obj[key]; + } + + return obj; +}; diff --git a/src/utils/hasRef.ts b/src/utils/hasRef.ts deleted file mode 100644 index 3d42e5f2e..000000000 --- a/src/utils/hasRef.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const hasRef = (obj: object): obj is object & { $ref: string } => - '$ref' in obj && typeof (obj as Partial<{ $ref: unknown }>).$ref === 'string'; diff --git a/src/utils/index.ts b/src/utils/index.ts index 85f1d7b01..5bc65387c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,7 @@ -export * from './hasRef'; +export * from './empty'; export * from './hasIntersectingElement'; export * from './isObject'; +export * from './refs'; +export * from './prepareResults'; +export * from './printPath'; +export * from './sortResults'; diff --git a/src/utils/prepareResults.ts b/src/utils/prepareResults.ts new file mode 100644 index 000000000..26001c541 --- /dev/null +++ b/src/utils/prepareResults.ts @@ -0,0 +1,46 @@ +const md5 = require('blueimp-md5'); + +import { uniqBy } from 'lodash'; + +import { IRuleResult } from '../types'; +import { compareResults } from './sortResults'; + +export type ComputeFingerprintFunc = (rule: IRuleResult, hash: (val: string) => string) => string; + +export const defaultComputeResultFingerprint: ComputeFingerprintFunc = (rule, hash) => { + let id = String(rule.code); + + if (rule.path.length) { + id += JSON.stringify(rule.path); + } else if (rule.range) { + id += JSON.stringify(rule.range); + } + + if (rule.source) id += rule.source; + + return hash(id); +}; + +export const prepareResults = (results: IRuleResult[], computeFingerprint: ComputeFingerprintFunc) => { + decorateResultsWithFingerprint(results, computeFingerprint); + + return sortResults(deduplicateResults(results)); +}; + +const decorateResultsWithFingerprint = (results: IRuleResult[], computeFingerprint: ComputeFingerprintFunc) => { + for (const r of results) { + Object.defineProperty(r, 'fingerprint', { + value: computeFingerprint(r, md5), + }); + } + + return results; +}; + +const deduplicateResults = (results: IRuleResult[]): IRuleResult[] => { + return uniqBy([...results], 'fingerprint'); +}; + +const sortResults = (results: IRuleResult[]): IRuleResult[] => { + return [...results].sort(compareResults); +}; diff --git a/src/utils/printPath.ts b/src/utils/printPath.ts new file mode 100644 index 000000000..884ab968c --- /dev/null +++ b/src/utils/printPath.ts @@ -0,0 +1,56 @@ +import { decodePointerFragment, pathToPointer } from '@stoplight/json'; +import { JsonPath, Segment } from '@stoplight/types'; + +export enum PrintStyle { + Dot = 'dot', + Pointer = 'pointer', + EscapedPointer = 'escapedPointer', +} + +const isNumeric = (input: Segment) => typeof input === 'number' || !Number.isNaN(Number(input)); +const hasWhitespace = (input: string) => /\s/.test(input); +const safeDecodePointerFragment = (segment: Segment) => + typeof segment === 'number' ? segment : decodePointerFragment(segment); + +const printDotBracketsSegment = (segment: Segment) => { + if (typeof segment === 'number') { + return `[${segment}]`; + } + + if (segment.length === 0) { + return `['']`; + } + + if (hasWhitespace(segment)) { + return `['${segment}']`; + } + + if (isNumeric(segment)) { + return `[${segment}]`; + } + + return null; +}; + +const pathToDotString = (path: JsonPath) => + path.reduce( + (output, segment, index) => `${output}${printDotBracketsSegment(segment) ?? `${index === 0 ? '' : '.'}${segment}`}`, + '', + ); + +export const printPath = (path: JsonPath, style: PrintStyle) => { + switch (style) { + case PrintStyle.Dot: + return decodePointerFragment(pathToDotString(path)); + case PrintStyle.Pointer: + if (path.length === 0) { + return '#'; + } + + return `#/${decodePointerFragment(path.join('/'))}`; + case PrintStyle.EscapedPointer: + return pathToPointer(path.map(safeDecodePointerFragment)); + default: + return String(path); + } +}; diff --git a/src/utils/refs.ts b/src/utils/refs.ts new file mode 100644 index 000000000..fbc0f023b --- /dev/null +++ b/src/utils/refs.ts @@ -0,0 +1,62 @@ +import { extractPointerFromRef, hasRef, pointerToPath } from '@stoplight/json'; +import { isAbsolute } from '@stoplight/path'; +import { Dictionary, JsonPath } from '@stoplight/types'; +import { isObject } from 'lodash'; + +export const isAbsoluteRef = (ref: string) => isAbsolute(ref) || /^[a-z]+:\/\//i.test(ref); + +export const traverseObjUntilRef = (obj: unknown, path: JsonPath): string | null => { + let piece: unknown = obj; + + for (const segment of path.slice()) { + if (!isObject(piece)) { + throw new TypeError('Segment is not a part of the object'); + } + + if (segment in piece) { + piece = piece[segment]; + } else if (hasRef(piece)) { + return piece.$ref; + } else { + throw new Error('Segment is not a part of the object'); + } + + path.shift(); + } + + if (isObject(piece) && hasRef(piece) && Object.keys(piece).length === 1) { + return piece.$ref; + } + + return null; +}; + +export const getEndRef = (refMap: Dictionary, $ref: string): string => { + while ($ref in refMap) { + $ref = refMap[$ref]; + } + + return $ref; +}; + +export const safePointerToPath = (pointer: string): JsonPath => { + const rawPointer = extractPointerFromRef(pointer); + return rawPointer ? pointerToPath(rawPointer) : []; +}; + +export const getClosestJsonPath = (data: unknown, path: JsonPath) => { + const closestPath: JsonPath = []; + + if (!isObject(data)) return closestPath; + + let piece = data; + + for (const segment of path) { + if (!(segment in piece)) break; + closestPath.push(segment); + if (!isObject(piece[segment])) break; + piece = piece[segment]; + } + + return closestPath; +}; diff --git a/src/utils/replacer.ts b/src/utils/replacer.ts new file mode 100644 index 000000000..077dbced1 --- /dev/null +++ b/src/utils/replacer.ts @@ -0,0 +1,36 @@ +import { Dictionary } from '@stoplight/types'; + +export type Transformer = (identifier: string, value: V, values: VV) => string; + +export class Replacer { + protected readonly regex: RegExp; + protected readonly transformers: Dictionary>; + + constructor(count: number) { + this.regex = new RegExp(`${'{'.repeat(count)}([^}\n]+)${'}'.repeat(count)}`, 'g'); + + this.transformers = {}; + } + + public addTransformer(name: string, filter: Transformer) { + this.transformers[name] = filter; + } + + public print(input: string, values: V) { + return input.replace(this.regex, (substr, expr) => { + const [identifier, ...transformers] = expr.split('|'); + + if (!(identifier in values)) { + return ''; + } + + for (const transformer of transformers) { + if (transformer in this.transformers) { + values[identifier] = this.transformers[transformer](identifier, values[identifier], values); + } + } + + return String(values[identifier]); + }); + } +} diff --git a/src/utils/sortResults.ts b/src/utils/sortResults.ts new file mode 100644 index 000000000..29238078d --- /dev/null +++ b/src/utils/sortResults.ts @@ -0,0 +1,79 @@ +import { IRuleResult } from '../types'; + +const compareCode = (left: string | number | undefined, right: string | number | undefined): number => { + if (left === void 0 && right === void 0) { + return 0; + } + + if (left === void 0) { + return -1; + } + + if (right === void 0) { + return 1; + } + + return String(left).localeCompare(String(right), void 0, { numeric: true }); +}; + +const compareSource = (left: string | undefined, right: string | undefined): number => { + if (left === void 0 && right === void 0) { + return 0; + } + + if (left === void 0) { + return -1; + } + + if (right === void 0) { + return 1; + } + + return left.localeCompare(right); +}; + +const normalize = (value: number): -1 | 0 | 1 => { + if (value < 0) { + return -1; + } + + if (value > 0) { + return 1; + } + + return 0; +}; + +export const compareResults = (left: IRuleResult, right: IRuleResult): -1 | 0 | 1 => { + const diffSource = compareSource(left.source, right.source); + + if (diffSource !== 0) { + return normalize(diffSource); + } + + const diffLine = left.range.start.line - right.range.start.line; + + if (diffLine !== 0) { + return normalize(diffLine); + } + + const diffChar = left.range.start.character - right.range.start.character; + + if (diffChar !== 0) { + return normalize(diffChar); + } + + const diffCode = compareCode(left.code, right.code); + + if (diffCode !== 0) { + return normalize(diffCode); + } + + const diffPath = left.path.join().localeCompare(right.path.join()); + + return normalize(diffPath); +}; + +export const sortResults = (results: IRuleResult[]) => { + return [...results].sort(compareResults); +}; diff --git a/test-harness/README.md b/test-harness/README.md index 81fa1c64c..87bfcabb2 100644 --- a/test-harness/README.md +++ b/test-harness/README.md @@ -35,7 +35,7 @@ Test text, can be multi line. ====document==== some JSON/YAML document ====command==== -lint --foo {document} --bar +{bin} lint --foo {document} --bar ====stdout==== expected output ====stderr==== @@ -73,7 +73,7 @@ paths: 'application/json': example: hello ====command==== -lint {document} +{bin} lint {document} ====stdout==== OpenAPI 3.x detected @@ -83,17 +83,57 @@ OpenAPI 3.x detected ✖ 1 problems (1 error, 0 warning, 0 infos, 0 hints) ``` +#### Custom assets + +Apart from `{document}`, you are allowed to provide any extra fixture your test may require. +An example of such a fixture could be a ruleset, but also a you would like to output Spectral validation results to. + +The syntax varies a bit from regular keywords and can be expressed in the following way `asset:` +where pattern is any string matching `[A-Za-z0-9.\-]` regular expression, for example `asset:my-ruleset` or `asset:petstore.oas2.json`. + +##### A real example? + +``` +====test==== +assets real-life example +====document==== +openapi: 3.0.0 +info: + version: 1.0.0 + title: Stoplight +paths: {} +====asset:ruleset==== +extends: spectral:oas +rules: + oas3-api-servers: error +====command==== +{bin} lint {document} -r {asset:ruleset} +====status==== +1 +====stdout==== +OpenAPI 3.x detected + +{document} + 1:1 error oas3-api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning openapi-tags OpenAPI object should have non-empty `tags` array. + 2:6 warning info-contact Info object should contain `contact` object. + 2:6 warning info-description OpenAPI object info `description` must be present and non-empty string. + +✖ 4 problems (1 error, 3 warnings, 0 infos, 0 hints) +``` + + #### Things to keep in mind when creating the files: * 1 test per file, we do not support multiple splitting. * Be precise with the separators. They should be 4 *before* **AND** *after* the word. `====` -* The keywords are `test`, `document`, `command`, `status`, `stdout`, and `stderr`, nothing else at the moment +* The keywords are `test`, `document`, `command`, `env`, `asset:`, `status`, `stdout`, and `stderr`, nothing else at the moment * You can run all the tests on the same port `4010`, but you can also choose another one -* You can pipe your command to grep. For example, using `lint {document} | grep 'expected-rule-name'` can be used to test for the presence of a specific violation. But, beware of being overly specific here. +* You can pipe your command to grep. For example, using `{bin} lint {document} | grep 'expected-rule-name'` can be used to test for the presence of a specific violation. But, beware of being overly specific here. ## Technical details * A RegExp is used to split the content -* A temporary file with the document is stored on your disk +* Temporary files with the document and/or any specified assets are stored on your disk * Spectral gets spawned with the specified arguments and output is matched -* `{document}` can be used in command, stdout or stderr, and is replaced with the full file path +* `{document}` and `{asset:}` can be used in command, stdout or stderr, and is replaced with the full file path diff --git a/test-harness/helpers.ts b/test-harness/helpers.ts index 527c6796d..c838a1056 100644 --- a/test-harness/helpers.ts +++ b/test-harness/helpers.ts @@ -1,23 +1,63 @@ -export function parseScenarioFile(data: string) { - const regex = /====(test|document|command|status|stdout|stderr|env)====\r?\n/gi; +import { Dictionary, Optional } from '@stoplight/types'; +import * as tmp from 'tmp'; + +export interface IScenarioFile { + test: string; + assets: string[][]; + command: string; + status: Optional; + stdout: Optional; + stderr: Optional; + env: typeof process.env; +} + +function getItem(input: string[], key: string, required: boolean): string; +function getItem(input: string[], key: string): Optional; +function getItem(input: string[], key: string, required?: boolean): Optional | string { + const index = input.findIndex(t => t === key); + if (index === -1 || index === input.length - 1) { + if (required) { + throw new TypeError(`Expected "${key}" to be provided`); + } + + return; + } + + return input[index + 1].trim(); +} + +export function parseScenarioFile(data: string): IScenarioFile { + const regex = /====(test|document|command|status|stdout|stderr|env|asset:[a-z0-9.\-]+)====\r?\n/gi; const split = data.split(regex); - const testIndex = split.findIndex(t => t === 'test'); - const documentIndex = split.findIndex(t => t === 'document'); - const commandIndex = split.findIndex(t => t === 'command'); - const statusIndex = split.findIndex(t => t === 'status'); - const stdoutIndex = split.findIndex(t => t === 'stdout'); - const stderrIndex = split.findIndex(t => t === 'stderr'); - const envIndex = split.findIndex(t => t === 'env'); + const test = getItem(split, 'test', true); + const document = getItem(split, 'document'); + const command = getItem(split, 'command', true); + const status = getItem(split, 'status'); + const stdout = getItem(split, 'stdout'); + const stderr = getItem(split, 'stderr'); + const env = getItem(split, 'env'); + + const assets = split.reduce((filtered, item, i) => { + if (item.startsWith('asset')) { + filtered.push([item, split[i + 1].trim()]); + } + + return filtered; + }, []); + + if (document !== void 0) { + assets.push(['document', document]); + } return { - test: split[1 + testIndex], - document: split[1 + documentIndex], - command: split[1 + commandIndex], - status: split[1 + statusIndex], - stdout: split[1 + stdoutIndex], - stderr: split[1 + stderrIndex], - env: envIndex === -1 ? process.env : getEnv(split[1 + envIndex]), + test, + assets, + command, + status, + stdout, + stderr, + env: env === void 0 ? process.env : getEnv(env), }; } @@ -31,3 +71,44 @@ function getEnv(env: string): NodeJS.ProcessEnv { { ...process.env }, ); } + +export function tmpFile(opts?: tmp.TmpNameOptions): Promise { + return new Promise((resolve, reject) => { + tmp.file( + { + postfix: '.yml', + prefix: 'asset-', + tries: 10, + ...opts, + }, + (err, name, fd, removeCallback) => { + if (err) { + reject(err); + } else { + resolve({ + name, + fd, + removeCallback, + }); + } + }, + ); + }); +} + +const BRACES = /{([^}]+)}/g; + +export const applyReplacements = (str: string, values: Dictionary) => { + BRACES.lastIndex = 0; + let result: RegExpExecArray | null; + + // tslint:disable-next-line:no-conditional-assignment + while ((result = BRACES.exec(str))) { + if (!(result[1] in values)) continue; + const newValue = String(values[result[1]] || ''); + str = `${str.slice(0, result.index)}${newValue}${str.slice(BRACES.lastIndex)}`; + BRACES.lastIndex = result.index + newValue.length; + } + + return str; +}; diff --git a/test-harness/index.ts b/test-harness/index.ts index 8ce384a40..45dcaa292 100644 --- a/test-harness/index.ts +++ b/test-harness/index.ts @@ -1,105 +1,74 @@ -import { spawnSync } from 'child_process'; +import * as path from '@stoplight/path'; +import { normalize } from '@stoplight/path'; +import { Dictionary } from '@stoplight/types'; +import * as fg from 'fast-glob'; import * as fs from 'fs'; -// @ts-ignore -import * as globFs from 'glob-fs'; -import * as os from 'os'; -import * as path from 'path'; -// @ts-ignore import * as tmp from 'tmp'; -import { parseScenarioFile } from './helpers'; - -const glob = globFs({ gitignore: true }); +import { promisify } from 'util'; +import { applyReplacements, parseScenarioFile, tmpFile } from './helpers'; +import { spawnNode } from './spawn'; +const writeFileAsync = promisify(fs.writeFile); const spectralBin = path.join(__dirname, '../binaries/spectral'); - -type Replacement = { - from: RegExp; - to: string; -}; - -function replaceVars(string: string, replacements: Replacement[]) { - return replacements.reduce((str, replace) => str.replace(replace.from, replace.to), string); -} +const cwd = path.join(__dirname, './scenarios'); +const files = process.env.TESTS ? String(process.env.TESTS).split(',') : fg.sync('**/*.scenario', { cwd }); describe('cli acceptance tests', () => { - const cwd = path.join(__dirname, './scenarios'); - const files = process.env.TESTS ? String(process.env.TESTS).split(',') : glob.readdirSync('**/*.scenario', { cwd }); - - files.forEach((file: string) => { + describe.each(files)('%s file', file => { const data = fs.readFileSync(path.join(cwd, file), { encoding: 'utf8' }); const scenario = parseScenarioFile(data); - const replacements: Replacement[] = []; - - let tmpFileHandle: tmp.FileSyncObject; - - beforeAll(() => { - if (scenario.document) { - tmpFileHandle = tmp.fileSync({ - postfix: '.yml', - dir: undefined, - name: undefined, - prefix: undefined, - tries: 10, - template: undefined, - unsafeCleanup: undefined, - }); - - replacements.push({ - from: /\{document\}/g, - to: tmpFileHandle.name, - }); - - replacements.push({ - from: /\{documentWithoutExt\}/g, - to: tmpFileHandle.name.replace(/\.yml$/, ''), - }); - - fs.writeFileSync(tmpFileHandle.name, scenario.document, { encoding: 'utf8' }); - } + const replacements: Dictionary = { + __dirname: normalize(__dirname), + bin: spectralBin, + }; + + const tmpFileHandles = new Map(); + + beforeAll(async () => { + await Promise.all( + scenario.assets.map(async ([asset, contents]) => { + const tmpFileHandle = await tmpFile(); + tmpFileHandles.set(asset, tmpFileHandle); + + replacements[asset] = tmpFileHandle.name; + replacements[`${asset}|no-ext`] = tmpFileHandle.name.replace( + new RegExp(`${path.extname(tmpFileHandle.name)}$`), + '', + ); + + await writeFileAsync(tmpFileHandle.name, contents, { encoding: 'utf8' }); // todo: apply replacements to contents + }), + ); }); afterAll(() => { - if (scenario.document) { - tmpFileHandle.removeCallback(undefined, undefined, undefined, undefined); + for (const { removeCallback } of tmpFileHandles.values()) { + removeCallback(); } - }); - test(`./test-harness/scenarios/${file}${os.EOL}${scenario.test}`, () => { - // TODO split on " " is going to break quoted args - const args = scenario.command.split(' ').map(t => { - const arg = t.trim(); - if (scenario.document && arg === '{document}') { - return tmpFileHandle.name; - } - return arg; - }); + tmpFileHandles.clear(); + }); - const commandHandle = spawnSync(spectralBin, args, { - shell: true, - encoding: 'utf8', - windowsVerbatimArguments: false, - env: scenario.env, - }); + test(scenario.test, async () => { + const command = applyReplacements(scenario.command, replacements); + const { stderr, stdout, status } = await spawnNode(command, scenario.env); + replacements.date = String(new Date()); // this may introduce random failures, but hopefully they don't occur too often - const expectedStatus = replaceVars(scenario.status.trim(), replacements); - const expectedStdout = replaceVars(scenario.stdout.trim(), replacements); - const expectedStderr = replaceVars(scenario.stderr.trim(), replacements); - const status = commandHandle.status; - const stderr = commandHandle.stderr.trim(); - const stdout = commandHandle.stdout.trim(); + const expectedStdout = scenario.stdout === void 0 ? void 0 : applyReplacements(scenario.stdout, replacements); + const expectedStderr = scenario.stderr === void 0 ? void 0 : applyReplacements(scenario.stderr, replacements); - if (expectedStderr) { + if (expectedStderr !== void 0) { expect(stderr).toEqual(expectedStderr); } else if (stderr) { throw new Error(stderr); } - if (stdout || expectedStdout) { + if (expectedStdout !== void 0) { expect(stdout).toEqual(expectedStdout); } - if (expectedStatus !== '') { - expect(`status:${status}`).toEqual(`status:${expectedStatus}`); + if (scenario.status !== void 0) { + expect(`status:${status}`).toEqual(`status:${scenario.status}`); } }); }); diff --git a/test-harness/scenarios/alphabetical-responses-order.oas3.scenario b/test-harness/scenarios/alphabetical-responses-order.oas3.scenario new file mode 100644 index 000000000..e76cad549 --- /dev/null +++ b/test-harness/scenarios/alphabetical-responses-order.oas3.scenario @@ -0,0 +1,33 @@ +====test==== +Responses can be sorted alphabetically +====document==== +openapi: 3.0.2 +info: + title: Test Spec + version: 0.0.0 +paths: + /foo: + get: + operationId: get-foo + responses: + '400': + description: '' + '200': + description: '' +====asset:ruleset==== +rules: + response-order: + message: Responses should be in alphabetical order + recommended: true + given: $.paths.*.*.responses + then: + function: alphabetical +====command==== +{bin} lint {document} --ruleset {asset:ruleset} +====stdout==== +OpenAPI 3.x detected + +{document} + 10:15 warning response-order Responses should be in alphabetical order + +✖ 1 problem (0 errors, 1 warning, 0 infos, 0 hints) diff --git a/test-harness/scenarios/custom-ref-resolver.scenario b/test-harness/scenarios/custom-ref-resolver.scenario new file mode 100644 index 000000000..e3501cebc --- /dev/null +++ b/test-harness/scenarios/custom-ref-resolver.scenario @@ -0,0 +1,15 @@ +====test==== +Custom json-ref-resolver instance is used for $ref resolving +====document==== +$ref: custom://user.yaml +====asset:ruleset==== +rules: + required-user: + given: $ + then: + field: user + function: truthy +====command==== +{bin} lint {document} --ruleset {asset:ruleset} --resolver ./resolvers/custom.js --ignore-unknown-format +====stdout==== +No results with a severity of 'error' or higher found! diff --git a/test-harness/scenarios/enabled-rules-amount.oas3.scenario b/test-harness/scenarios/enabled-rules-amount.oas3.scenario index 8052a5269..06df64ecd 100644 --- a/test-harness/scenarios/enabled-rules-amount.oas3.scenario +++ b/test-harness/scenarios/enabled-rules-amount.oas3.scenario @@ -12,7 +12,7 @@ paths: - name: ok in: header responses: - 200: + '200': description: abc components: parameters: @@ -22,9 +22,9 @@ components: schema: type: integer ====command==== -lint {document} --ruleset ./test-harness/scenarios/rulesets/parameter-description.oas3.yaml -v +{bin} lint {document} --ruleset ./rulesets/parameter-description.oas3.yaml -v ====stdout==== -Found 52 rules (1 enabled) +Found 53 rules (1 enabled) Linting {document} OpenAPI 3.x detected diff --git a/test-harness/scenarios/external-schemas-ruleset.scenario b/test-harness/scenarios/external-schemas-ruleset.scenario index 2b0d1415b..3aa363c05 100644 --- a/test-harness/scenarios/external-schemas-ruleset.scenario +++ b/test-harness/scenarios/external-schemas-ruleset.scenario @@ -6,12 +6,12 @@ info: title: foo description: baz ====command==== -lint {document} --ruleset ./test-harness/scenarios/rulesets/external-schemas-ruleset.yaml +{bin} lint {document} --ruleset ./rulesets/external-schemas-ruleset.yaml ====stdout==== OpenAPI 3.x detected {document} - 3:10 error info-title should be equal to one of the allowed values: Stoplight, Stoplight.io, StoplightIO. Did you mean Stoplight? - 4:16 error info-description should be equal to one of the allowed values: foo, foo-bar, bar-foo + 3:10 error info-title string should be equal to one of the allowed values: Stoplight, Stoplight.io, StoplightIO. Did you mean Stoplight? + 4:16 error info-description string should be equal to one of the allowed values: foo, foo-bar, bar-foo ✖ 2 problems (2 errors, 0 warnings, 0 infos, 0 hints) diff --git a/test-harness/scenarios/help-no-document.scenario b/test-harness/scenarios/help-no-document.scenario index c3527e630..cde7525d0 100644 --- a/test-harness/scenarios/help-no-document.scenario +++ b/test-harness/scenarios/help-no-document.scenario @@ -1,14 +1,14 @@ ====test==== Errors when no document is provided ====command==== -lint -====stderr==== -spectral lint +./scripts/faketty.bash {bin} lint +====stdout==== +spectral lint [documents..] lint JSON/YAML documents from files or URLs Positionals: - documents Location of JSON/YAML documents. Can be either a file, a glob or fetchable resource(s) on the web. [array] [required] [default: []] + documents Location of JSON/YAML documents. Can be either a file, a glob or fetchable resource(s) on the web. [array] [default: []] Options: --version Show version number [boolean] @@ -16,9 +16,11 @@ Options: --encoding, -e text encoding to use [string] [default: "utf8"] --format, -f formatter to use for outputting results [string] [default: "stylish"] --output, -o output to a file instead of stdout [string] + --resolver path to custom json-ref-resolver instance [string] --ruleset, -r path/URL to a ruleset file [string] --skip-rule, -s ignore certain rules if they are causing trouble [string] - --fail-severity, -F results of this level or above will trigger a failure exit code [string] [choices: "error", "warn", "info", "hint"] [default: "hint"] + --fail-severity, -F results of this level or above will trigger a failure exit code [string] [choices: "error", "warn", "info", "hint"] [default: "error"] --display-only-failures, -D only output results equal to or greater than --fail-severity [boolean] [default: false] + --ignore-unknown-format do not warn about unmatched formats [boolean] [default: false] --verbose, -v increase verbosity [boolean] --quiet, -q no logging - output only [boolean] diff --git a/test-harness/scenarios/ignored-unrecognized-format.scenario b/test-harness/scenarios/ignored-unrecognized-format.scenario new file mode 100644 index 000000000..a93011d21 --- /dev/null +++ b/test-harness/scenarios/ignored-unrecognized-format.scenario @@ -0,0 +1,9 @@ +====test==== +Does not report unrecognized formats given --ignore-unknown-format +====document==== +openapi: 4.0.0 +info: {} +====command==== +{bin} lint {document} --ignore-unknown-format +====stdout==== +No results with a severity of 'error' or higher found! diff --git a/test-harness/scenarios/invalid-custom-ref-resolver.scenario b/test-harness/scenarios/invalid-custom-ref-resolver.scenario new file mode 100644 index 000000000..05e485774 --- /dev/null +++ b/test-harness/scenarios/invalid-custom-ref-resolver.scenario @@ -0,0 +1,17 @@ +====test==== +Prints meaningful error message when custom json-ref-resolver instance cannot be imported +====document==== +$ref: custom://user.yaml +====asset:ruleset==== +rules: + required-user: + given: $ + then: + field: user + function: truthy +====command==== +{bin} lint {document} --ruleset {asset:ruleset} --resolver ./resolvers/missing-resolver.js +====status==== +2 +====stderr==== +Cannot find module '{__dirname}/scenarios/resolvers/missing-resolver.js' diff --git a/test-harness/scenarios/parameter-description-links.oas3.scenario b/test-harness/scenarios/parameter-description-links.oas3.scenario index ef4405165..b5dc956c7 100644 --- a/test-harness/scenarios/parameter-description-links.oas3.scenario +++ b/test-harness/scenarios/parameter-description-links.oas3.scenario @@ -6,7 +6,7 @@ paths: /pets: get: responses: - 200: + '200': description: Expected response to a valid request links: abc: @@ -22,7 +22,7 @@ components: parameters: userUuid: abc ====command==== -lint --ruleset ./test-harness/scenarios/rulesets/parameter-description.oas3.yaml {document} +{bin} lint --ruleset ./rulesets/parameter-description.oas3.yaml {document} ====stdout==== OpenAPI 3.x detected -No results with a severity of 'hint' or higher found! +No results with a severity of 'error' or higher found! diff --git a/test-harness/scenarios/parameter-description-parameters.oas2.scenario b/test-harness/scenarios/parameter-description-parameters.oas2.scenario index 048fbfc38..6c9ae7d26 100644 --- a/test-harness/scenarios/parameter-description-parameters.oas2.scenario +++ b/test-harness/scenarios/parameter-description-parameters.oas2.scenario @@ -22,7 +22,7 @@ paths: in: query description: abc responses: - 200: + '200': description: abc parameters: skipParam: @@ -35,7 +35,7 @@ parameters: type: string description: status ====command==== -lint {document} --ruleset ./test-harness/scenarios/rulesets/parameter-description.oas2.yaml +{bin} lint {document} --ruleset ./rulesets/parameter-description.oas2.yaml ====stdout==== OpenAPI 2.0 (Swagger) detected diff --git a/test-harness/scenarios/parameter-description-parameters.oas3.scenario b/test-harness/scenarios/parameter-description-parameters.oas3.scenario index b387376ee..37dd0182d 100644 --- a/test-harness/scenarios/parameter-description-parameters.oas3.scenario +++ b/test-harness/scenarios/parameter-description-parameters.oas3.scenario @@ -12,7 +12,7 @@ paths: - name: ok in: header responses: - 200: + '200': description: abc components: parameters: @@ -22,7 +22,7 @@ components: schema: type: integer ====command==== -lint {document} --ruleset ./test-harness/scenarios/rulesets/parameter-description.oas3.yaml +{bin} lint {document} --ruleset ./rulesets/parameter-description.oas3.yaml ====stdout==== OpenAPI 3.x detected diff --git a/test-harness/scenarios/proxy-agent.scenario b/test-harness/scenarios/proxy-agent.scenario index 3906ad302..5735ecff5 100644 --- a/test-harness/scenarios/proxy-agent.scenario +++ b/test-harness/scenarios/proxy-agent.scenario @@ -6,9 +6,8 @@ foo: ====env==== PROXY=http://localhost:3001 ====command==== -lint {document} +{bin} lint {document} --ignore-unknown-format ====stdout==== - {document} 2:9 error invalid-ref FetchError: request to http://localhost:3002/foo.json failed, reason: connect ECONNREFUSED 127.0.0.1:3001 diff --git a/test-harness/scenarios/resolvers/custom.js b/test-harness/scenarios/resolvers/custom.js new file mode 100644 index 000000000..9e25f319d --- /dev/null +++ b/test-harness/scenarios/resolvers/custom.js @@ -0,0 +1,11 @@ +const { Resolver } = require('@stoplight/json-ref-resolver'); + +module.exports = new Resolver({ + resolvers: { + custom: { + async resolve() { + return `{ "user": "Stoplight" }`; + } + } + } +}); diff --git a/test-harness/scenarios/results-default-format-json-quiet.oas3.scenario b/test-harness/scenarios/results-default-format-json-quiet.oas3.scenario index 632853306..be3362d31 100644 --- a/test-harness/scenarios/results-default-format-json-quiet.oas3.scenario +++ b/test-harness/scenarios/results-default-format-json-quiet.oas3.scenario @@ -6,12 +6,14 @@ info: version: 1.0.0 title: Stoplight paths: {} +tags: + - name: test ====command==== -lint --quiet {document} --format=json +{bin} lint --quiet {document} --format=json ====stdout==== [ { - "code": "api-servers", + "code": "oas3-api-servers", "path": [], "message": "OpenAPI `servers` must be present and non-empty array.", "severity": 1, @@ -21,8 +23,8 @@ lint --quiet {document} --format=json "character": 0 }, "end": { - "line": 4, - "character": 9 + "line": 6, + "character": 14 } }, "source": "{document}" @@ -65,4 +67,4 @@ lint --quiet {document} --format=json }, "source": "{document}" } -] \ No newline at end of file +] diff --git a/test-harness/scenarios/results-default-format-json.oas3.scenario b/test-harness/scenarios/results-default-format-json.oas3.scenario index 58dea7b61..9ccebd328 100644 --- a/test-harness/scenarios/results-default-format-json.oas3.scenario +++ b/test-harness/scenarios/results-default-format-json.oas3.scenario @@ -6,13 +6,15 @@ info: version: 1.0.0 title: Stoplight paths: {} +tags: + - name: test ====command==== -lint {document} --format=json +{bin} lint {document} --format=json ====stdout==== OpenAPI 3.x detected [ { - "code": "api-servers", + "code": "oas3-api-servers", "path": [], "message": "OpenAPI `servers` must be present and non-empty array.", "severity": 1, @@ -22,8 +24,8 @@ OpenAPI 3.x detected "character": 0 }, "end": { - "line": 4, - "character": 9 + "line": 6, + "character": 14 } }, "source": "{document}" @@ -66,4 +68,4 @@ OpenAPI 3.x detected }, "source": "{document}" } -] \ No newline at end of file +] diff --git a/test-harness/scenarios/results-default-output.oas3.scenario b/test-harness/scenarios/results-default-output.oas3.scenario index 29b34533b..99ee87e03 100644 --- a/test-harness/scenarios/results-default-output.oas3.scenario +++ b/test-harness/scenarios/results-default-output.oas3.scenario @@ -8,11 +8,13 @@ info: title: Stoplight paths: {} ====command==== -lint {document} --output=./test-harness/tmp/results-default-output.txt > /dev/null; cat ./test-harness/tmp/results-default-output.txt +{bin} lint {document} --output={asset:output.txt} > /dev/null; cat {asset:output.txt} +====asset:output.txt==== ====stdout==== {document} - 1:1 warning api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning oas3-api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning openapi-tags OpenAPI object should have non-empty `tags` array. 2:6 warning info-contact Info object should contain `contact` object. 2:6 warning info-description OpenAPI object info `description` must be present and non-empty string. -✖ 3 problems (0 errors, 3 warnings, 0 infos, 0 hints) +✖ 4 problems (0 errors, 4 warnings, 0 infos, 0 hints) diff --git a/test-harness/scenarios/results-default.oas3.scenario b/test-harness/scenarios/results-default.oas3.scenario index e77c7d127..f37ac7b05 100644 --- a/test-harness/scenarios/results-default.oas3.scenario +++ b/test-harness/scenarios/results-default.oas3.scenario @@ -7,13 +7,14 @@ info: title: Stoplight paths: {} ====command==== -lint {document} +{bin} lint {document} ====stdout==== OpenAPI 3.x detected {document} - 1:1 warning api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning oas3-api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning openapi-tags OpenAPI object should have non-empty `tags` array. 2:6 warning info-contact Info object should contain `contact` object. 2:6 warning info-description OpenAPI object info `description` must be present and non-empty string. -✖ 3 problems (0 errors, 3 warnings, 0 infos, 0 hints) +✖ 4 problems (0 errors, 4 warnings, 0 infos, 0 hints) diff --git a/test-harness/scenarios/results-format-html.oas3.scenario b/test-harness/scenarios/results-format-html.oas3.scenario new file mode 100644 index 000000000..f8db3fea8 --- /dev/null +++ b/test-harness/scenarios/results-format-html.oas3.scenario @@ -0,0 +1,159 @@ +====test==== +Invalid OAS3 document outputs results when --format=html +====document==== +openapi: 3.0.0 +info: + version: 1.0.0 + title: Stoplight +paths: {} +====command==== +{bin} lint {document} --format=html +====stdout==== +OpenAPI 3.x detected + + + + + + Spectral Report + + +
+

Spectral Report

+
+ 4 problems (0 errors, 4 warnings, 0 infos, 0 hints) - Generated on {date} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ [+] {document} + 4 problems (0 errors, 4 warnings, 0 infos, 0 hints) +
+ + + diff --git a/test-harness/scenarios/results-format-junit.oas3.scenario b/test-harness/scenarios/results-format-junit.oas3.scenario index 948dc69e5..94765faa2 100644 --- a/test-harness/scenarios/results-format-junit.oas3.scenario +++ b/test-harness/scenarios/results-format-junit.oas3.scenario @@ -8,12 +8,12 @@ info: foo: paths: {} ====command==== -lint {document} --format=junit +{bin} lint {document} --format=junit ====stdout==== OpenAPI 3.x detected - + diff --git a/test-harness/scenarios/results-format-stylish.oas3.scenario b/test-harness/scenarios/results-format-stylish.oas3.scenario index 480367a72..4cf53dc85 100644 --- a/test-harness/scenarios/results-format-stylish.oas3.scenario +++ b/test-harness/scenarios/results-format-stylish.oas3.scenario @@ -7,13 +7,14 @@ info: title: Stoplight paths: {} ====command==== -lint {document} +{bin} lint {document} ====stdout==== OpenAPI 3.x detected {document} - 1:1 warning api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning oas3-api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning openapi-tags OpenAPI object should have non-empty `tags` array. 2:6 warning info-contact Info object should contain `contact` object. 2:6 warning info-description OpenAPI object info `description` must be present and non-empty string. -✖ 3 problems (0 errors, 3 warnings, 0 infos, 0 hints) +✖ 4 problems (0 errors, 4 warnings, 0 infos, 0 hints) diff --git a/test-harness/scenarios/results-skip-rule.oas3.scenario b/test-harness/scenarios/results-skip-rule.oas3.scenario index 8b60c124b..1e38b335a 100644 --- a/test-harness/scenarios/results-skip-rule.oas3.scenario +++ b/test-harness/scenarios/results-skip-rule.oas3.scenario @@ -7,12 +7,13 @@ info: title: Stoplight paths: {} ====command==== -lint {document} --skip-rule=info-contact +{bin} lint {document} --skip-rule=info-contact ====stdout==== OpenAPI 3.x detected {document} - 1:1 warning api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning oas3-api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning openapi-tags OpenAPI object should have non-empty `tags` array. 2:6 warning info-description OpenAPI object info `description` must be present and non-empty string. -✖ 2 problems (0 errors, 2 warnings, 0 infos, 0 hints) +✖ 3 problems (0 errors, 3 warnings, 0 infos, 0 hints) diff --git a/test-harness/scenarios/results-skip-rules-multiple.oas3.scenario b/test-harness/scenarios/results-skip-rules-multiple.oas3.scenario index 2024a99f6..bc686f08f 100644 --- a/test-harness/scenarios/results-skip-rules-multiple.oas3.scenario +++ b/test-harness/scenarios/results-skip-rules-multiple.oas3.scenario @@ -7,11 +7,12 @@ info: title: Stoplight paths: {} ====command==== -lint {document} --skip-rule=info-contact --skip-rule=info-description +{bin} lint {document} --skip-rule=info-contact --skip-rule=info-description ====stdout==== OpenAPI 3.x detected {document} - 1:1 warning api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning oas3-api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning openapi-tags OpenAPI object should have non-empty `tags` array. -✖ 1 problem (0 errors, 1 warning, 0 infos, 0 hints) +✖ 2 problems (0 errors, 2 warnings, 0 infos, 0 hints) diff --git a/test-harness/scenarios/rules-matching-multiple-places.scenario b/test-harness/scenarios/rules-matching-multiple-places.scenario new file mode 100644 index 000000000..6d26fe3c0 --- /dev/null +++ b/test-harness/scenarios/rules-matching-multiple-places.scenario @@ -0,0 +1,41 @@ +====test==== +Rules matching multiple properties in the document +====document==== +schemas: + user: + type: object + properties: + name: + type: number + age: + type: number + occupation: + type: boolean + address: +====command==== +{bin} lint {document} -r {asset:ruleset} +====asset:ruleset==== +rules: + valid-user-properties: + severity: error + given: [$.schemas.user.properties.name, $.schemas.user.properties.occupation] + then: + field: type + function: pattern + functionOptions: + match: /^string$/ + require-user-and-address: + severity: error + given: [$.schemas.user, $.schemas.address] + then: + function: truthy +====status==== +1 +====stdout==== +{document} + 1:1 warning unrecognized-format The provided document does not match any of the registered formats [oas2, oas3, json-schema, json-schema-loose, json-schema-draft4, json-schema-draft6, json-schema-draft7, json-schema-2019-09] + 6:15 error valid-user-properties must match the pattern '/^string$/' + 10:15 error valid-user-properties must match the pattern '/^string$/' + 11:11 error require-user-and-address `address` property is not truthy + +✖ 4 problems (3 errors, 1 warning, 0 infos, 0 hints) diff --git a/test-harness/scenarios/rulesets/loose-schema-ruleset.json b/test-harness/scenarios/rulesets/loose-schema-ruleset.json deleted file mode 100644 index d527e94fa..000000000 --- a/test-harness/scenarios/rulesets/loose-schema-ruleset.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "formats": ["json-schema-loose"], - "rules": { - "no-root-all-of": { - "description": "No root allOf combiner", - "message": "{{description}}", - "recommended": true, - "severity": "error", - "type": "validation", - "given": "$", - "then": { - "field": "allOf", - "function": "falsy" - } - }, - "no-empty-description": { - "description": "Description must not be empty", - "message": "{{description}}", - "recommended": true, - "type": "validation", - "given": "$..description", - "then": { - "function": "truthy" - } - } - } -} diff --git a/test-harness/scenarios/rulesets/parameter-description.oas2.yaml b/test-harness/scenarios/rulesets/parameter-description.oas2.yaml index dc82fa3aa..773a2473a 100644 --- a/test-harness/scenarios/rulesets/parameter-description.oas2.yaml +++ b/test-harness/scenarios/rulesets/parameter-description.oas2.yaml @@ -1,3 +1,3 @@ -extends: [[spectral:oas2, off]] +extends: [[spectral:oas, off]] rules: oas2-parameter-description: true diff --git a/test-harness/scenarios/rulesets/parameter-description.oas3.yaml b/test-harness/scenarios/rulesets/parameter-description.oas3.yaml index dbc1f0fdf..f2a50acaf 100644 --- a/test-harness/scenarios/rulesets/parameter-description.oas3.yaml +++ b/test-harness/scenarios/rulesets/parameter-description.oas3.yaml @@ -1,3 +1,3 @@ -extends: [[spectral:oas3, off]] +extends: [[spectral:oas, off]] rules: oas3-parameter-description: true diff --git a/test-harness/scenarios/scripts/faketty.bash b/test-harness/scenarios/scripts/faketty.bash new file mode 100755 index 000000000..d8faa3dc7 --- /dev/null +++ b/test-harness/scenarios/scripts/faketty.bash @@ -0,0 +1,3 @@ +#!/bin/bash + +script -qfec "$(printf "%q " "$@")" /dev/null diff --git a/test-harness/scenarios/severity/display-errors.oas3.scenario b/test-harness/scenarios/severity/display-errors.oas3.scenario index 9faa2210c..506a4e8b5 100644 --- a/test-harness/scenarios/severity/display-errors.oas3.scenario +++ b/test-harness/scenarios/severity/display-errors.oas3.scenario @@ -39,7 +39,7 @@ paths: tag: type: string ====command==== -lint {document} --fail-severity=error -D +{bin} lint {document} --fail-severity=error -D ====status==== 0 ====stdout==== diff --git a/test-harness/scenarios/severity/display-warnings.oas3.scenario b/test-harness/scenarios/severity/display-warnings.oas3.scenario index d752660a6..2f75fc350 100644 --- a/test-harness/scenarios/severity/display-warnings.oas3.scenario +++ b/test-harness/scenarios/severity/display-warnings.oas3.scenario @@ -1,5 +1,5 @@ ====test==== -Fail severity is set to error but only warnings exist, +Fail severity is set to error but only warnings exist, so status should be success and output should show warnings ====document==== openapi: '3.0.0' @@ -40,16 +40,18 @@ paths: tag: type: string ====command==== -lint {document} --fail-severity=error +{bin} lint {document} --fail-severity=error ====status==== 0 ====stdout==== OpenAPI 3.x detected {document} - 1:1 warning api-servers OpenAPI `servers` must be present and non-empty array. - 2:6 warning info-contact Info object should contain `contact` object. - 2:6 warning info-description OpenAPI object info `description` must be present and non-empty string. - 9:9 warning operation-description Operation `description` must be present and non-empty string. + 1:1 warning oas3-api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning openapi-tags OpenAPI object should have non-empty `tags` array. + 2:6 warning info-contact Info object should contain `contact` object. + 2:6 warning info-description OpenAPI object info `description` must be present and non-empty string. + 9:9 warning operation-description Operation `description` must be present and non-empty string. + 13:11 warning operation-tag-defined Operation tags should be defined in global tags. -✖ 4 problems (0 errors, 4 warnings, 0 infos, 0 hints) +✖ 6 problems (0 errors, 6 warnings, 0 infos, 0 hints) diff --git a/test-harness/scenarios/severity/fail-on-error-no-error.scenario b/test-harness/scenarios/severity/fail-on-error-no-error.scenario index 23dc7e784..ef1aa6a70 100644 --- a/test-harness/scenarios/severity/fail-on-error-no-error.scenario +++ b/test-harness/scenarios/severity/fail-on-error-no-error.scenario @@ -10,22 +10,23 @@ paths: get: operationId: foo responses: - 200: + '200': description: ok post: operationId: bar responses: - 200: + '200': description: ok ====command==== -lint {document} --fail-severity=error +{bin} lint {document} --fail-severity=error ====status==== 0 ====stdout==== OpenAPI 3.x detected {document} - 1:1 warning api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning oas3-api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning openapi-tags OpenAPI object should have non-empty `tags` array. 2:6 warning info-contact Info object should contain `contact` object. 2:6 warning info-description OpenAPI object info `description` must be present and non-empty string. 7:9 warning operation-description Operation `description` must be present and non-empty string. @@ -33,4 +34,4 @@ OpenAPI 3.x detected 12:10 warning operation-description Operation `description` must be present and non-empty string. 12:10 warning operation-tags Operation should have non-empty `tags` array. -✖ 7 problems (0 errors, 7 warnings, 0 infos, 0 hints) +✖ 8 problems (0 errors, 8 warnings, 0 infos, 0 hints) diff --git a/test-harness/scenarios/severity/fail-on-error.oas3.scenario b/test-harness/scenarios/severity/fail-on-error.oas3.scenario index 23e995632..dd03874f7 100644 --- a/test-harness/scenarios/severity/fail-on-error.oas3.scenario +++ b/test-harness/scenarios/severity/fail-on-error.oas3.scenario @@ -18,21 +18,24 @@ paths: 200: description: ok ====command==== -lint {document} --fail-severity=error +{bin} lint {document} --fail-severity=error ====status==== 1 ====stdout==== OpenAPI 3.x detected {document} - 1:1 warning api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning oas3-api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning openapi-tags OpenAPI object should have non-empty `tags` array. 2:6 warning info-contact Info object should contain `contact` object. 2:6 warning info-description OpenAPI object info `description` must be present and non-empty string. 7:9 warning operation-description Operation `description` must be present and non-empty string. 7:9 warning operation-tags Operation should have non-empty `tags` array. 8:20 error operation-operationId-unique Every operation must have a unique `operationId`. + 10:9 error parser Mapping key must be a string scalar rather than number 12:10 warning operation-description Operation `description` must be present and non-empty string. 12:10 warning operation-tags Operation should have non-empty `tags` array. 13:20 error operation-operationId-unique Every operation must have a unique `operationId`. + 15:9 error parser Mapping key must be a string scalar rather than number -✖ 9 problems (2 errors, 7 warnings, 0 infos, 0 hints) +✖ 12 problems (4 errors, 8 warnings, 0 infos, 0 hints) diff --git a/test-harness/scenarios/severity/stylish-display-proper-names.scenario b/test-harness/scenarios/severity/stylish-display-proper-names.scenario index 1f0efe029..4fcd78738 100644 --- a/test-harness/scenarios/severity/stylish-display-proper-names.scenario +++ b/test-harness/scenarios/severity/stylish-display-proper-names.scenario @@ -82,20 +82,44 @@ The name of severity levels are display correctly by stylish formatter } } } - +====asset:oas-mixed-severity.json==== +{ + "extends": ["spectral:oas"], + "rules": { + "info-contact": "error", + "operation-description": "info", + "info-matches-stoplight": { + "message": "Info must contain Stoplight", + "given": "$.info", + "severity": "hint", + "type": "style", + "recommended": true, + "then": { + "field": "title", + "function": "pattern", + "functionOptions": { + "match": "Stoplight" + } + } + } + } +} ====command==== -lint --ruleset ./test-harness/scenarios/rulesets/oas-mixed-severity.json {document} +{bin} lint --ruleset {asset:oas-mixed-severity.json} {document} ====stdout==== OpenAPI 3.x detected {document} - 1:1 warning api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning oas3-api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning openapi-tags OpenAPI object should have non-empty `tags` array. 3:10 error info-contact Info object should contain `contact` object. 3:10 warning info-description OpenAPI object info `description` must be present and non-empty string. 5:14 hint info-matches-stoplight Info must contain Stoplight 12:13 information operation-description Operation `description` must be present and non-empty string. + 15:18 warning operation-tag-defined Operation tags should be defined in global tags. 42:27 error invalid-ref '#/components/schemas/Pets' does not exist 52:27 error invalid-ref '#/components/schemas/Error' does not exist 59:14 information operation-description Operation `description` must be present and non-empty string. + 62:18 warning operation-tag-defined Operation tags should be defined in global tags. -✖ 8 problems (3 errors, 2 warnings, 2 infos, 1 hint) +✖ 11 problems (3 errors, 5 warnings, 2 infos, 1 hint) diff --git a/test-harness/scenarios/specs/petstore.oas2.json b/test-harness/scenarios/specs/petstore.oas2.json new file mode 100644 index 000000000..51c8faa18 --- /dev/null +++ b/test-harness/scenarios/specs/petstore.oas2.json @@ -0,0 +1,889 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.", + "version": "1.0.0", + "title": "Swagger Petstore", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "email": "apiteam@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "host": "petstore.swagger.io", + "basePath": "/v2", + "tags": [ + { + "name": "pet", + "description": "Everything about your Pets", + "externalDocs": { + "description": "Find out more", + "url": "http://swagger.io" + } + }, + { + "name": "store", + "description": "Access to Petstore orders" + }, + { + "name": "user", + "description": "Operations about user", + "externalDocs": { + "description": "Find out more about our store", + "url": "http://swagger.io" + } + } + ], + "schemes": ["https", "http"], + "paths": { + "/pet": { + "post": { + "tags": ["pet"], + "summary": "Add a new pet to the store", + "description": "", + "operationId": "addPet", + "consumes": ["application/json", "application/xml"], + "produces": ["application/xml", "application/json"], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + } + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": ["write:pets", "read:pets"] + } + ] + }, + "put": { + "tags": ["pet"], + "summary": "Update an existing pet", + "description": "", + "operationId": "updatePet", + "consumes": ["application/json", "application/xml"], + "produces": ["application/xml", "application/json"], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + } + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "405": { + "description": "Validation exception" + } + }, + "security": [ + { + "petstore_auth": ["write:pets", "read:pets"] + } + ] + } + }, + "/pet/findByStatus": { + "get": { + "tags": ["pet"], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "produces": ["application/xml", "application/json"], + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Status values that need to be considered for filter", + "required": true, + "type": "array", + "items": { + "type": "string", + "enum": ["available", "pending", "sold"], + "default": "available" + }, + "collectionFormat": "multi" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "description": "Invalid status value" + } + }, + "security": [ + { + "petstore_auth": ["write:pets", "read:pets"] + } + ] + } + }, + "/pet/findByTags": { + "get": { + "tags": ["pet"], + "summary": "Finds Pets by tags", + "description": "Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "produces": ["application/xml", "application/json"], + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "Tags to filter by", + "required": true, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "description": "Invalid tag value" + } + }, + "security": [ + { + "petstore_auth": ["write:pets", "read:pets"] + } + ], + "deprecated": true + } + }, + "/pet/{petId}": { + "get": { + "tags": ["pet"], + "summary": "Find pet by ID", + "description": "Returns a single pet", + "operationId": "getPetById", + "produces": ["application/xml", "application/json"], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to return", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Pet" + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "security": [ + { + "api_key": [] + } + ] + }, + "post": { + "tags": ["pet"], + "summary": "Updates a pet in the store with form data", + "description": "", + "operationId": "updatePetWithForm", + "consumes": ["application/x-www-form-urlencoded"], + "produces": ["application/xml", "application/json"], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "name", + "in": "formData", + "description": "Updated name of the pet", + "required": false, + "type": "string" + }, + { + "name": "status", + "in": "formData", + "description": "Updated status of the pet", + "required": false, + "type": "string" + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": ["write:pets", "read:pets"] + } + ] + }, + "delete": { + "tags": ["pet"], + "summary": "Deletes a pet", + "description": "", + "operationId": "deletePet", + "produces": ["application/xml", "application/json"], + "parameters": [ + { + "name": "api_key", + "in": "header", + "required": false, + "type": "string" + }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "security": [ + { + "petstore_auth": ["write:pets", "read:pets"] + } + ] + } + }, + "/pet/{petId}/uploadImage": { + "post": { + "tags": ["pet"], + "summary": "uploads an image", + "description": "", + "operationId": "uploadFile", + "consumes": ["multipart/form-data"], + "produces": ["application/json"], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to update", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "additionalMetadata", + "in": "formData", + "description": "Additional data to pass to server", + "required": false, + "type": "string" + }, + { + "name": "file", + "in": "formData", + "description": "file to upload", + "required": false, + "type": "file" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/ApiResponse" + } + } + }, + "security": [ + { + "petstore_auth": ["write:pets", "read:pets"] + } + ] + } + }, + "/store/inventory": { + "get": { + "tags": ["store"], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "produces": ["application/json"], + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/store/order": { + "post": { + "tags": ["store"], + "summary": "Place an order for a pet", + "description": "", + "operationId": "placeOrder", + "produces": ["application/xml", "application/json"], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "order placed for purchasing the pet", + "required": true, + "schema": { + "$ref": "#/definitions/Order" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Order" + } + }, + "400": { + "description": "Invalid Order" + } + } + } + }, + "/store/order/{orderId}": { + "get": { + "tags": ["store"], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions", + "operationId": "getOrderById", + "produces": ["application/xml", "application/json"], + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of pet that needs to be fetched", + "required": true, + "type": "integer", + "maximum": 10, + "minimum": 1, + "format": "int64" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Order" + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + }, + "delete": { + "tags": ["store"], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors", + "operationId": "deleteOrder", + "produces": ["application/xml", "application/json"], + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of the order that needs to be deleted", + "required": true, + "type": "integer", + "minimum": 1, + "format": "int64" + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + } + }, + "/user": { + "post": { + "tags": ["user"], + "summary": "Create user", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "produces": ["application/xml", "application/json"], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Created user object", + "required": true, + "schema": { + "$ref": "#/definitions/User" + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/createWithArray": { + "post": { + "tags": ["user"], + "summary": "Creates list of users with given input array", + "description": "", + "operationId": "createUsersWithArrayInput", + "produces": ["application/xml", "application/json"], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "List of user object", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/createWithList": { + "post": { + "tags": ["user"], + "summary": "Creates list of users with given input array", + "description": "", + "operationId": "createUsersWithListInput", + "produces": ["application/xml", "application/json"], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "List of user object", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/login": { + "get": { + "tags": ["user"], + "summary": "Logs user into the system", + "description": "", + "operationId": "loginUser", + "produces": ["application/xml", "application/json"], + "parameters": [ + { + "name": "username", + "in": "query", + "description": "The user name for login", + "required": true, + "type": "string" + }, + { + "name": "password", + "in": "query", + "description": "The password for login in clear text", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "string" + }, + "headers": { + "X-Rate-Limit": { + "type": "integer", + "format": "int32", + "description": "calls per hour allowed by the user" + }, + "X-Expires-After": { + "type": "string", + "format": "date-time", + "description": "date in UTC when token expires" + } + } + }, + "400": { + "description": "Invalid username/password supplied" + } + } + } + }, + "/user/logout": { + "get": { + "tags": ["user"], + "summary": "Logs out current logged in user session", + "description": "", + "operationId": "logoutUser", + "produces": ["application/xml", "application/json"], + "parameters": [], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/{username}": { + "get": { + "tags": ["user"], + "summary": "Get user by user name", + "description": "", + "operationId": "getUserByName", + "produces": ["application/xml", "application/json"], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be fetched. Use user1 for testing. ", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/User" + } + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "put": { + "tags": ["user"], + "summary": "Updated user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "produces": ["application/xml", "application/json"], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "name that need to be updated", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "description": "Updated user object", + "required": true, + "schema": { + "$ref": "#/definitions/User" + } + } + ], + "responses": { + "400": { + "description": "Invalid user supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "delete": { + "tags": ["user"], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "produces": ["application/xml", "application/json"], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be deleted", + "required": true, + "type": "string" + } + ], + "responses": { + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + } + } + }, + "securityDefinitions": { + "petstore_auth": { + "type": "oauth2", + "authorizationUrl": "https://petstore.swagger.io/oauth/dialog", + "flow": "implicit", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + }, + "definitions": { + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "petId": { + "type": "integer", + "format": "int64" + }, + "quantity": { + "type": "integer", + "format": "int32" + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "enum": ["placed", "approved", "delivered"] + }, + "complete": { + "type": "boolean", + "default": false + } + }, + "xml": { + "name": "Order" + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "userStatus": { + "type": "integer", + "format": "int32", + "description": "User Status" + } + }, + "xml": { + "name": "User" + } + }, + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Category" + } + }, + "Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Tag" + } + }, + "Pet": { + "type": "object", + "required": ["name", "photoUrls"], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "category": { + "$ref": "#/definitions/Category" + }, + "name": { + "type": "string", + "example": "doggie" + }, + "photoUrls": { + "type": "array", + "xml": { + "name": "photoUrl", + "wrapped": true + }, + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "xml": { + "name": "tag", + "wrapped": true + }, + "items": { + "$ref": "#/definitions/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": ["available", "pending", "sold"] + } + }, + "xml": { + "name": "Pet" + } + }, + "ApiResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + }, + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + } +} diff --git a/test-harness/scenarios/stdin-document-with-errors.scenario b/test-harness/scenarios/stdin-document-with-errors.scenario new file mode 100644 index 000000000..9f802b535 --- /dev/null +++ b/test-harness/scenarios/stdin-document-with-errors.scenario @@ -0,0 +1,26 @@ +====test==== +Lints stdin input +====document==== +openapi: 3.0.0 +info: + version: 1.0.0 + title: Stoplight +paths: {} +====asset:ruleset==== +extends: spectral:oas +rules: + oas3-api-servers: error +====command==== +cat {document} | {bin} lint -r {asset:ruleset} +====status==== +1 +====stdout==== +OpenAPI 3.x detected + + + 1:1 error oas3-api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning openapi-tags OpenAPI object should have non-empty `tags` array. + 2:6 warning info-contact Info object should contain `contact` object. + 2:6 warning info-description OpenAPI object info `description` must be present and non-empty string. + +✖ 4 problems (1 error, 3 warnings, 0 infos, 0 hints) diff --git a/test-harness/scenarios/stdin.scenario b/test-harness/scenarios/stdin.scenario new file mode 100644 index 000000000..fb56fd060 --- /dev/null +++ b/test-harness/scenarios/stdin.scenario @@ -0,0 +1,20 @@ +====test==== +Lints stdin input +====document==== +openapi: 3.0.0 +info: + version: 1.0.0 + title: Stoplight +paths: {} +====command==== +cat {document} | {bin} lint +====stdout==== +OpenAPI 3.x detected + + + 1:1 warning oas3-api-servers OpenAPI `servers` must be present and non-empty array. + 1:1 warning openapi-tags OpenAPI object should have non-empty `tags` array. + 2:6 warning info-contact Info object should contain `contact` object. + 2:6 warning info-description OpenAPI object info `description` must be present and non-empty string. + +✖ 4 problems (0 errors, 4 warnings, 0 infos, 0 hints) diff --git a/test-harness/scenarios/todo-full-loose-schema.scenario b/test-harness/scenarios/todo-full-loose-schema.scenario index d0efd1f23..da0318fc4 100644 --- a/test-harness/scenarios/todo-full-loose-schema.scenario +++ b/test-harness/scenarios/todo-full-loose-schema.scenario @@ -55,8 +55,36 @@ Loose JSON Schema can be validated } ] } +====asset:loose-schema-ruleset==== +{ + "formats": ["json-schema-loose"], + "rules": { + "no-root-all-of": { + "description": "No root allOf combiner", + "message": "{{description}}", + "recommended": true, + "severity": "error", + "type": "validation", + "given": "$", + "then": { + "field": "allOf", + "function": "falsy" + } + }, + "no-empty-description": { + "description": "Description must not be empty", + "message": "{{description}}", + "recommended": true, + "type": "validation", + "given": "$..description", + "then": { + "function": "truthy" + } + } + } +} ====command==== -lint {document} --ruleset ./test-harness/scenarios/rulesets/loose-schema-ruleset.json +{bin} lint {document} --ruleset {asset:loose-schema-ruleset} ====stdout==== JSON Schema (loose) detected diff --git a/test-harness/scenarios/unrecognized-format.scenario b/test-harness/scenarios/unrecognized-format.scenario new file mode 100644 index 000000000..9c97b0ff7 --- /dev/null +++ b/test-harness/scenarios/unrecognized-format.scenario @@ -0,0 +1,12 @@ +====test==== +Reports unrecognized formats +====document==== +openapi: 4.0.0 +info: {} +====command==== +{bin} lint {document} +====stdout==== +{document} + 1:1 warning unrecognized-format The provided document does not match any of the registered formats [oas2, oas3, json-schema, json-schema-loose, json-schema-draft4, json-schema-draft6, json-schema-draft7, json-schema-2019-09] + +✖ 1 problem (0 errors, 1 warning, 0 infos, 0 hints) diff --git a/test-harness/scenarios/valid-no-errors.oas2.scenario b/test-harness/scenarios/valid-no-errors.oas2.scenario index eb00d47fa..f710a6ce9 100644 --- a/test-harness/scenarios/valid-no-errors.oas2.scenario +++ b/test-harness/scenarios/valid-no-errors.oas2.scenario @@ -12,8 +12,10 @@ host: localhost schemes: - http paths: {} +tags: + - name: my-tag ====command==== -lint {document} +{bin} lint {document} ====stdout==== OpenAPI 2.0 (Swagger) detected -No results with a severity of 'hint' or higher found! +No results with a severity of 'error' or higher found! diff --git a/test-harness/spawn.ts b/test-harness/spawn.ts new file mode 100644 index 000000000..fe781b0c0 --- /dev/null +++ b/test-harness/spawn.ts @@ -0,0 +1,71 @@ +import { join } from '@stoplight/path'; +import { Optional } from '@stoplight/types'; +import * as child_process from 'child_process'; +import { Transform } from 'stream'; + +const cwd = join(__dirname, 'scenarios'); + +export type SpawnReturn = { + stdout: string; + stderr: string; + status: number; +}; + +export type SpawnFn = (command: string, env: Optional) => Promise; + +const createStream = () => + new Transform({ + transform(chunk, encoding, done) { + this.push(chunk); + done(); + }, + }); + +function stringifyStream(stream: Transform) { + let result = ''; + + stream.on('readable', () => { + let chunk: string | null; + + // tslint:disable-next-line:no-conditional-assignment + while ((chunk = stream.read()) !== null) { + result += chunk; + } + }); + + return new Promise((resolve, reject) => { + stream.on('error', reject); + stream.on('end', () => { + resolve(result); + }); + }); +} + +export const spawnNode: SpawnFn = async (script, env) => { + const stderr = createStream(); + const stdout = createStream(); + + const handle = child_process.spawn(script, [], { + shell: true, + windowsVerbatimArguments: false, + env, + cwd, + stdio: 'pipe', + }); + + handle.stderr.pipe(stderr); + handle.stdout.pipe(stdout); + + const stderrText = (await stringifyStream(stderr)).trim(); + const stdoutText = (await stringifyStream(stdout)).trim(); + + const status = await new Promise(resolve => { + handle.on('close', resolve); + }); + + return { + stderr: stderrText.replace(/\r?\n/g, '\n'), + stdout: stdoutText.replace(/\r?\n/g, '\n'), + status, + }; +}; diff --git a/tsconfig.json b/tsconfig.json index ff828ad7c..6980c9dc4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "include": ["src"], "compilerOptions": { "moduleResolution": "node", - "target": "es6", + "target": "es2017", "module": "commonjs", "lib": ["es2015", "es2016", "es2017", "dom"], "strict": true, diff --git a/yarn.lock b/yarn.lock index 467f09d6e..7294b436f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,18 +10,18 @@ "@babel/highlight" "^7.0.0" "@babel/core@^7.1.0": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.5.tgz#17b2686ef0d6bc58f963dddd68ab669755582c30" - integrity sha512-i4qoSr2KTtce0DmkuuQBV4AuQgGPUcPXMr9L5MyYAtk06z068lQ10a4O009fe5OB/DfNV+h+qqT7ddNV8UnRjg== + version "7.7.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.2.tgz#ea5b99693bcfc058116f42fa1dd54da412b29d91" + integrity sha512-eeD7VEZKfhK1KUXGiyPFettgF3m513f8FoBSWiQ1xTvl1RAopLs42Wp9+Ze911I6H0N9lNqJMDgoZT7gHsipeQ== dependencies: "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.5.5" - "@babel/helpers" "^7.5.5" - "@babel/parser" "^7.5.5" - "@babel/template" "^7.4.4" - "@babel/traverse" "^7.5.5" - "@babel/types" "^7.5.5" - convert-source-map "^1.1.0" + "@babel/generator" "^7.7.2" + "@babel/helpers" "^7.7.0" + "@babel/parser" "^7.7.2" + "@babel/template" "^7.7.0" + "@babel/traverse" "^7.7.2" + "@babel/types" "^7.7.2" + convert-source-map "^1.7.0" debug "^4.1.0" json5 "^2.1.0" lodash "^4.17.13" @@ -29,53 +29,52 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.4.0", "@babel/generator@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.5.5.tgz#873a7f936a3c89491b43536d12245b626664e3cf" - integrity sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ== +"@babel/generator@^7.4.0", "@babel/generator@^7.7.2": + version "7.7.2" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.7.2.tgz#2f4852d04131a5e17ea4f6645488b5da66ebf3af" + integrity sha512-WthSArvAjYLz4TcbKOi88me+KmDJdKSlfwwN8CnUYn9jBkzhq0ZEPuBfkAWIvjJ3AdEV1Cf/+eSQTnp3IDJKlQ== dependencies: - "@babel/types" "^7.5.5" + "@babel/types" "^7.7.2" jsesc "^2.5.1" lodash "^4.17.13" source-map "^0.5.0" - trim-right "^1.0.1" -"@babel/helper-function-name@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53" - integrity sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw== +"@babel/helper-function-name@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.7.0.tgz#44a5ad151cfff8ed2599c91682dda2ec2c8430a3" + integrity sha512-tDsJgMUAP00Ugv8O2aGEua5I2apkaQO7lBGUq1ocwN3G23JE5Dcq0uh3GvFTChPa4b40AWiAsLvCZOA2rdnQ7Q== dependencies: - "@babel/helper-get-function-arity" "^7.0.0" - "@babel/template" "^7.1.0" - "@babel/types" "^7.0.0" + "@babel/helper-get-function-arity" "^7.7.0" + "@babel/template" "^7.7.0" + "@babel/types" "^7.7.0" -"@babel/helper-get-function-arity@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3" - integrity sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ== +"@babel/helper-get-function-arity@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.0.tgz#c604886bc97287a1d1398092bc666bc3d7d7aa2d" + integrity sha512-tLdojOTz4vWcEnHWHCuPN5P85JLZWbm5Fx5ZsMEMPhF3Uoe3O7awrbM2nQ04bDOUToH/2tH/ezKEOR8zEYzqyw== dependencies: - "@babel/types" "^7.0.0" + "@babel/types" "^7.7.0" "@babel/helper-plugin-utils@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA== -"@babel/helper-split-export-declaration@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" - integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q== +"@babel/helper-split-export-declaration@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.0.tgz#1365e74ea6c614deeb56ebffabd71006a0eb2300" + integrity sha512-HgYSI8rH08neWlAH3CcdkFg9qX9YsZysZI5GD8LjhQib/mM0jGOZOVkoUiiV2Hu978fRtjtsGsW6w0pKHUWtqA== dependencies: - "@babel/types" "^7.4.4" + "@babel/types" "^7.7.0" -"@babel/helpers@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.5.5.tgz#63908d2a73942229d1e6685bc2a0e730dde3b75e" - integrity sha512-nRq2BUhxZFnfEn/ciJuhklHvFOqjJUD5wpx+1bxUF2axL9C+v4DE/dmp5sT2dKnpOs4orZWzpAZqlCy8QqE/7g== +"@babel/helpers@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.7.0.tgz#359bb5ac3b4726f7c1fde0ec75f64b3f4275d60b" + integrity sha512-VnNwL4YOhbejHb7x/b5F39Zdg5vIQpUUNzJwx0ww1EcVRt41bbGRZWhAURrfY32T5zTT3qwNOQFWpn+P0i0a2g== dependencies: - "@babel/template" "^7.4.4" - "@babel/traverse" "^7.5.5" - "@babel/types" "^7.5.5" + "@babel/template" "^7.7.0" + "@babel/traverse" "^7.7.0" + "@babel/types" "^7.7.0" "@babel/highlight@^7.0.0": version "7.5.0" @@ -86,15 +85,10 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.4.4", "@babel/parser@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b" - integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g== - -"@babel/parser@~7.4.4": - version "7.4.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872" - integrity sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew== +"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.7.0", "@babel/parser@^7.7.2", "@babel/parser@^7.7.5": + version "7.7.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.5.tgz#cbf45321619ac12d83363fcf9c94bb67fa646d71" + integrity sha512-KNlOe9+/nk4i29g0VXgl8PEXIRms5xKLJeuZ6UptN0fHv+jDiriG+y94X6qAgWTR0h3KaoM1wK5G5h7MHFRSig== "@babel/plugin-syntax-object-rest-spread@^7.0.0": version "7.2.0" @@ -103,48 +97,41 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/runtime@^7.0.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.0.tgz#4fc1d642a9fd0299754e8b5de62c631cf5568205" - integrity sha512-89eSBLJsxNxOERC0Op4vd+0Bqm6wRMqMbFtV3i0/fbaWw/mJ8Q3eBvgX0G4SyrOOLCtbu98HspF8o09MRT+KzQ== - dependencies: - regenerator-runtime "^0.13.2" - -"@babel/runtime@~7.4.4": - version "7.4.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12" - integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.7.5": + version "7.7.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.6.tgz#d18c511121aff1b4f2cd1d452f1bac9601dd830f" + integrity sha512-BWAJxpNVa0QlE5gZdWjSxXtemZyZ9RmrmVozxt3NUXeZhVIJ5ANyqmMc0JDrivBZyxUuQvFxlvH4OWWOogGfUw== dependencies: regenerator-runtime "^0.13.2" -"@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" - integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw== +"@babel/template@^7.4.0", "@babel/template@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.7.0.tgz#4fadc1b8e734d97f56de39c77de76f2562e597d0" + integrity sha512-OKcwSYOW1mhWbnTBgQY5lvg1Fxg+VyfQGjcBduZFljfc044J5iDlnDSfhQ867O17XHiSCxYHUxHg2b7ryitbUQ== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.4.4" - "@babel/types" "^7.4.4" + "@babel/parser" "^7.7.0" + "@babel/types" "^7.7.0" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.5.tgz#f664f8f368ed32988cd648da9f72d5ca70f165bb" - integrity sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.2": + version "7.7.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.7.2.tgz#ef0a65e07a2f3c550967366b3d9b62a2dcbeae09" + integrity sha512-TM01cXib2+rgIZrGJOLaHV/iZUAxf4A0dt5auY6KNZ+cm6aschuJGqKJM3ROTt3raPUdIDk9siAufIFEleRwtw== dependencies: "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.5.5" - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.4.4" - "@babel/parser" "^7.5.5" - "@babel/types" "^7.5.5" + "@babel/generator" "^7.7.2" + "@babel/helper-function-name" "^7.7.0" + "@babel/helper-split-export-declaration" "^7.7.0" + "@babel/parser" "^7.7.2" + "@babel/types" "^7.7.2" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.13" -"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.5.tgz#97b9f728e182785909aa4ab56264f090a028d18a" - integrity sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw== +"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.7.0", "@babel/types@^7.7.2": + version "7.7.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.7.2.tgz#550b82e5571dcd174af576e23f0adba7ffc683f7" + integrity sha512-YTf6PXoh3+eZgRCBzzP25Bugd2ngmpQVrk7kXX0i5N9BO7TFBtIgZYs7WtxtOGs8e6A4ZI7ECkbBCEHeXocvOA== dependencies: esutils "^2.0.2" lodash "^4.17.13" @@ -158,7 +145,7 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@commitlint/config-conventional@^8.1.0": +"@commitlint/config-conventional@^8.2.0": version "8.2.0" resolved "https://registry.yarnpkg.com/@commitlint/config-conventional/-/config-conventional-8.2.0.tgz#886a5538e3708e017ec2871e0cbce00f635d3102" integrity sha512-HuwlHQ3DyVhpK9GHgTMhJXD8Zp8PGIQVpQGYh/iTrEU6TVxdRC61BxIDZvfWatCaiG617Z/U8maRAFrqFM4TqA== @@ -311,14 +298,6 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^13.0.0" -"@mrmlnc/readdir-enhanced@^2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" - integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g== - dependencies: - call-me-maybe "^1.0.1" - glob-to-regexp "^0.3.0" - "@nodelib/fs.scandir@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" @@ -332,11 +311,6 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== -"@nodelib/fs.stat@^1.1.2": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" - integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== - "@nodelib/fs.walk@^1.2.3": version "1.2.4" resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" @@ -352,39 +326,58 @@ dependencies: any-observable "^0.3.0" -"@stoplight/json-ref-resolver@^2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@stoplight/json-ref-resolver/-/json-ref-resolver-2.2.0.tgz#f12fed6ca618d9cd6031f5d0405a3ae5ffd54fad" - integrity sha512-42z5MNkLKF7Ynq2rM8YSd6SoJvEX5k0ugQujpB8Co7K5m3vhq1LK+/faFhuMfuFy6As9BxVUnDbGchYERXK17Q== +"@stoplight/json-ref-readers@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@stoplight/json-ref-readers/-/json-ref-readers-1.1.1.tgz#7c6c7cce7ac01e840cf56eaee10f2476b6f4a644" + integrity sha512-yE6SpGaBlj+QM4ony1+ST1Unz4TimZglU1lSJOlyCrVrAC1VoFpoJ1exMnU8Cg/++YzmBN9qBa4jk9s0CBnrTA== + dependencies: + node-fetch "^2.6.0" + +"@stoplight/json-ref-resolver@^3.0.6": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@stoplight/json-ref-resolver/-/json-ref-resolver-3.0.6.tgz#ed39221c24ac7e39b5c4306fc6b262f729cc0f6b" + integrity sha512-EOuPiEeZb59AFhdF1fh1OBlGoJnQN/7djn5Rmmn57/AqY0n8A2UBRxbDPNwZKcrigX4qTkXlxqlHzXiG2GEByg== dependencies: - "@stoplight/json" "^3.1.0" - "@stoplight/types" "^11.0.0" + "@stoplight/json" "^3.1.2" + "@stoplight/path" "^1.3.0" + "@stoplight/types" "^11.1.1" + "@types/urijs" "^1.19" dependency-graph "~0.8.0" fast-memoize "^2.5.1" - immer "^3.2.0" + immer "^4.0.1" lodash "^4.17.15" + tslib "^1.10.0" urijs "~1.19.1" - vscode-uri "^2.0.3" -"@stoplight/json@^3.1.0", "@stoplight/json@^3.1.1": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@stoplight/json/-/json-3.1.2.tgz#ae89d1974f0b6f8bc5778f792aeed4664c95ec62" - integrity sha512-7zt0zCN94m90X1sL7FrD7jXhPtkIup07MyG/00sELVrCvzb2wgNaiE+x2N7WSbk3Z0wS08e8B6kow81GQY+Isw== +"@stoplight/json@^3.1.2": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@stoplight/json/-/json-3.2.1.tgz#cbc8080f4a55744beb89668d058e48ba52da141b" + integrity sha512-RnXApQlP6GEUHIK8JRianStXTc4dHARpNP8VVGL/BbctTTlA3BO+Z1eILu35JdqvMG5sw6Og3TkQyQ85w6sSoA== dependencies: - "@stoplight/types" "^11.0.0" - jsonc-parser "~2.1.1" + "@stoplight/types" "^11.1.1" + jsonc-parser "~2.2.0" lodash "^4.17.15" safe-stable-stringify "^1.1" -"@stoplight/path@^1.2.0": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@stoplight/path/-/path-1.2.1.tgz#f39f408377a0d1a9c33a04ce08eb3a1691c457d6" - integrity sha512-W/wBpTw6ThKZGPcb1ztKz6ULnMVnLE/ygzqIjnqnmKcpNrKkSuBLtL7+oQYZSkteZfJAryloAt3plU3sTiekcQ== +"@stoplight/json@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@stoplight/json/-/json-3.4.0.tgz#c2557ced0834a127b1ce524e77f15364d814047a" + integrity sha512-Ljjj11Wa+MusOMeXTehLbuJQJe8CG3sovCGcD/6c8Zyz1n39EsDLZwJIpQ6/DQAdKChiJGWdcULsrGdheFaiLg== + dependencies: + "@stoplight/types" "^11.1.1" + jsonc-parser "~2.2.0" + lodash "^4.17.15" + safe-stable-stringify "^1.1" + +"@stoplight/path@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@stoplight/path/-/path-1.3.0.tgz#da2282352a4eb23c09d5106b9d1650d30a9ca2ad" + integrity sha512-t74/MHMgmFVMQhdQ/2Q766GryNTIW8McH8+vB25oeoBhYKTOrJ/wPDt+OCxIWHPUlcSi2fTWa4FKQ8qgmP2jVA== -"@stoplight/types@^11.0.0", "@stoplight/types@^11.1.1": - version "11.1.1" - resolved "https://registry.yarnpkg.com/@stoplight/types/-/types-11.1.1.tgz#a92d1833adb580a72439f42ba73de8f560fd68b4" - integrity sha512-IU8U9y/uO548z15DX/Jl053u9VQG8gCwNtypuD4RtskUA7pvHZl4+zzGK3klgIcO6Ql3Jk4/fcrFaN9vjmdEWg== +"@stoplight/types@^11.1.1", "@stoplight/types@^11.2.0": + version "11.3.0" + resolved "https://registry.yarnpkg.com/@stoplight/types/-/types-11.3.0.tgz#23d40cbf2c3c85e5612038b5cf0c003f82809318" + integrity sha512-m6N4Bv2O2bYJOXdIwtpLtfQY/3zHUgRuHx2D8ERydX8WE40nKoLqh4wN6nvRcSeGwxjhi0Q0KUAZIe9UsYj6fQ== dependencies: "@types/json-schema" "^7.0.3" @@ -393,19 +386,19 @@ resolved "https://registry.yarnpkg.com/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.44.tgz#ed3c962564283e9983f7895a6effc3994286df5e" integrity sha512-PdY8p2Ufgtorf4d2DbKMfknILMa8KwuyyMMR/2lgK1mLaU8F5PKWYc+h9hIzC+ar0bh7m9h2rINo32m7ADfVyA== -"@stoplight/yaml@^3.1.0": - version "3.3.2" - resolved "https://registry.yarnpkg.com/@stoplight/yaml/-/yaml-3.3.2.tgz#001049ed4a8733fca43cc60efe6bcd046abef210" - integrity sha512-KfrEsl3bA8mtoIklVvS4Hg8OrOYYtqi+K0IsQ7lJbZLVaUA4wMwDGwz85a6YWeo1OuVe8tumM6OynrFIjFutNA== +"@stoplight/yaml@^3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@stoplight/yaml/-/yaml-3.5.0.tgz#ed5462c6b0b82ad492c42740bb816d343f4ed0ab" + integrity sha512-q4HlH4+U5GGL+LHImbz7w29q3M0fMF5c6jE/PbSth5MNBvUNXAX8rxyWWt017LadH8VIoDgnam/0dO/or5SCvQ== dependencies: "@stoplight/types" "^11.1.1" "@stoplight/yaml-ast-parser" "0.0.44" lodash "^4.17.15" "@types/babel__core@^7.1.0": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.2.tgz#608c74f55928033fce18b99b213c16be4b3d114f" - integrity sha512-cfCCrFmiGY/yq0NuKNxIQvZFy9kY/1immpSpTngOnyIbD4+eJOG5mxphhHDv3CHL9GltO4GcKr54kGBg3RNdbg== + version "7.1.3" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.3.tgz#e441ea7df63cd080dfcd02ab199e6d16a735fc30" + integrity sha512-8fBo0UR2CcwWxeX7WIIgJ7lXjasFxoYgRnFHUj+hRvKkpiBJbxhdAPTCY6/ZKM0uxANFVzt4yObSLuTiTnazDA== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" @@ -414,9 +407,9 @@ "@types/babel__traverse" "*" "@types/babel__generator@*": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.0.2.tgz#d2112a6b21fad600d7674274293c85dce0cb47fc" - integrity sha512-NHcOfab3Zw4q5sEE2COkpfXjoE7o+PmqD9DQW4koUT3roNxwziUdXGnRndMat/LJNUtePwn1TlP4do3uoe3KZQ== + version "7.6.0" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.0.tgz#f1ec1c104d1bb463556ecb724018ab788d0c172a" + integrity sha512-c1mZUu4up5cp9KROs/QAw0gTeHrw/x7m52LcnvMxxOZ03DmLwPV0MlGmlgzV3cnSdjhJOZsj7E7FHeioai+egw== dependencies: "@babel/types" "^7.0.0" @@ -442,7 +435,12 @@ dependencies: chalk "*" -"@types/estree@0.0.39": +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + +"@types/estree@*", "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== @@ -453,9 +451,9 @@ integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== "@types/fetch-mock@^7.3.1": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@types/fetch-mock/-/fetch-mock-7.3.1.tgz#df7421e8bcb351b430bfbfa5c52bb353826ac94f" - integrity sha512-2U4vZWHNbsbK7TRmizgr/pbKe0FKopcxu+hNDtIBDiM1wvrKRItybaYj7VQ6w/hZJStU/JxRiNi5ww4YDEvKbA== + version "7.3.2" + resolved "https://registry.yarnpkg.com/@types/fetch-mock/-/fetch-mock-7.3.2.tgz#58805ba36a9357be92cc8c008dbfda937e9f7d8f" + integrity sha512-NCEfv49jmDsBAixjMjEHKVgmVQlJ+uK56FOc+2roYPExnXCZDpi6mJOHQ3v23BiO84hBDStND9R2itJr7PNoow== "@types/glob@^7.1.1": version "7.1.1" @@ -486,24 +484,19 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" -"@types/jest-diff@*": - version "20.0.1" - resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89" - integrity sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA== - -"@types/jest-when@^2.4.0": +"@types/jest-when@^2.7.0": version "2.7.0" resolved "https://registry.yarnpkg.com/@types/jest-when/-/jest-when-2.7.0.tgz#538d5a3d069aedb496ce42aed76404fc8673bf27" integrity sha512-oahwfICJSaEmmtyN7JeOg6P7ey+GKigzk26zdKLxYLewmr2F3WeDPo/U+kVpIxPuZG3EiVFdRy97q9N9DoEkSg== dependencies: "@types/jest" "*" -"@types/jest@*", "@types/jest@^24.0.16": - version "24.0.18" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.18.tgz#9c7858d450c59e2164a8a9df0905fc5091944498" - integrity sha512-jcDDXdjTcrQzdN06+TSVsPPqxvsZA/5QkYfIZlq1JMw7FdP5AZylbOc+6B/cuDurctRe+MziUMtQ3xQdrbjqyQ== +"@types/jest@*", "@types/jest@^24.0.22": + version "24.0.24" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.24.tgz#0f2f523dc77cc1bc6bef34eaf287ede887a73f05" + integrity sha512-vgaG968EDPSJPMunEDdZvZgvxYSmeH8wKqBlHSkBt1pV2XlLEVDzsj1ZhLuI4iG4Pv841tES61txSBF0obh4CQ== dependencies: - "@types/jest-diff" "*" + jest-diff "^24.3.0" "@types/json-schema@^7.0.3": version "7.0.3" @@ -515,17 +508,17 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/lodash@^4.14.136": - version "4.14.141" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.141.tgz#d81f4d0c562abe28713406b571ffb27692a82ae6" - integrity sha512-v5NYIi9qEbFEUpCyikmnOYe4YlP8BMUdTcNCAquAKzu+FA7rZ1onj9x80mbnDdOW/K5bFf3Tv5kJplP33+gAbQ== +"@types/lodash@^4.14.146": + version "4.14.149" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440" + integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ== "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== -"@types/nanoid@^2.0.0": +"@types/nanoid@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@types/nanoid/-/nanoid-2.1.0.tgz#41edfda78986e9127d0dc14de982de766f994020" integrity sha512-xdkn/oRTA0GSNPLIKZgHWqDTWZsVrieKomxJBOQUK9YDD+zfSgmwD5t4WJYra5S7XyhTw7tfvwznW+pFexaepQ== @@ -539,22 +532,17 @@ dependencies: nock "*" -"@types/node-fetch@^2.5.0": - version "2.5.2" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.2.tgz#76906dea5b3d6901e50e63e15249c9bcd6e9676e" - integrity sha512-djYYKmdNRSBtL1x4CiE9UJb9yZhwtI1VC+UxZD0psNznrUj80ywsxKlEGAE+QL1qvLjPbfb24VosjkYM6W4RSQ== +"@types/node-fetch@^2.5.3": + version "2.5.4" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.4.tgz#5245b6d8841fc3a6208b82291119bc11c4e0ce44" + integrity sha512-Oz6id++2qAOFuOlE1j0ouk1dzl3mmI1+qINPNBhi9nt/gVOz0G+13Ao6qjhdF0Ys+eOkhu6JnFmt38bR3H0POQ== dependencies: "@types/node" "*" -"@types/node@*", "@types/node@^12.7.5", "@types/node@~12.7": - version "12.7.11" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.11.tgz#be879b52031cfb5d295b047f5462d8ef1a716446" - integrity sha512-Otxmr2rrZLKRYIybtdG/sgeO+tHY20GxeDjcGmUnmmlCWyEnv2a2x1ZXBo3BTec4OiTXMQCiazB8NMBf0iRlFw== - -"@types/node@^8.0.7": - version "8.10.54" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.54.tgz#1c88eb253ac1210f1a5876953fb70f7cc4928402" - integrity sha512-kaYyLYf6ICn6/isAyD4K1MyWWd5Q3JgH6bnMN089LUx88+s4W8GvK9Q6JMBVu5vsFFp7pMdSxdKmlBXwH/VFRg== +"@types/node@*", "@types/node@~12.12": + version "12.12.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.14.tgz#1c1d6e3c75dba466e0326948d56e8bd72a1903d2" + integrity sha512-u/SJDyXwuihpwjXy7hOOghagLEV1KdAST6syfnOk6QZAMzZuWZqXy5aYYZbh8Jdpd4escVFP0MvftHNDb9pruA== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -573,46 +561,46 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== -"@types/text-table@^0.2.0": +"@types/text-table@^0.2.1": version "0.2.1" resolved "https://registry.yarnpkg.com/@types/text-table/-/text-table-0.2.1.tgz#39c4d4a058a82f677392dfd09976e83d9b4c9264" integrity sha512-dchbFCWfVgUSWEvhOkXGS7zjm+K7jCUvGrQkAHPk2Fmslfofp4HQTH2pqnQ3Pw5GPYv0zWa2AQjKtsfZThuemQ== -"@types/xml2js@^0.4.4": +"@types/tmp@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.1.0.tgz#19cf73a7bcf641965485119726397a096f0049bd" + integrity sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA== + +"@types/urijs@^1.19": + version "1.19.4" + resolved "https://registry.yarnpkg.com/@types/urijs/-/urijs-1.19.4.tgz#29c4a694d4842d7f95e359a26223fc1865f1ab13" + integrity sha512-uHUvuLfy4YkRHL4UH8J8oRsINhdEHd9ymag7KJZVT94CjAmY1njoUzhazJsZjwfy+IpWKQKGVyXCwzhZvg73Fg== + +"@types/xml2js@^0.4.5": version "0.4.5" resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.4.5.tgz#d21759b056f282d9c7066f15bbf5c19b908f22fa" integrity sha512-yohU3zMn0fkhlape1nxXG2bLEGZRc1FeqF80RoHaYXJN7uibaauXfhzhOJr1Xh36sn+/tx21QAOf07b/xYVk1w== dependencies: "@types/node" "*" -"@types/yaml@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@types/yaml/-/yaml-1.0.2.tgz#bba080d64714c6ef3eaa023e235dacd2cfa3c938" - integrity sha512-rS1VJFjyGKNHk8H97COnPIK+oeLnc0J9G0ES63o/Ky+WlJCeaFGiGCTGhV/GEVKua7ZWIV1JIDopYUwrfvTo7A== - "@types/yargs-parser@*": - version "13.0.0" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.0.0.tgz#453743c5bbf9f1bed61d959baab5b06be029b2d0" - integrity sha512-wBlsw+8n21e6eTd4yVv8YD/E3xq0O6nNnJIquutAsFGE7EyMKz7W6RNT6BRu1SmdgmlCZ9tb0X+j+D6HGr8pZw== + version "13.1.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.1.0.tgz#c563aa192f39350a1d18da36c5a8da382bbd8228" + integrity sha512-gCubfBUZ6KxzoibJ+SCUc/57Ms1jz5NjHe4+dI2krNmU5zCPAphyLJYyTOg06ueIyfj+SaCUqmzun7ImlxDcKg== "@types/yargs@^13.0.0": - version "13.0.2" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.2.tgz#a64674fc0149574ecd90ba746e932b5a5f7b3653" - integrity sha512-lwwgizwk/bIIU+3ELORkyuOgDjCh7zuWDFqRtPPhhVgq9N1F7CvLNKg1TX4f2duwtKQ0p044Au9r1PLIXHrIzQ== + version "13.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.3.tgz#76482af3981d4412d65371a318f992d33464a380" + integrity sha512-K8/LfZq2duW33XW/tFwEAfnZlqIfVsoyRB3kfXdPXYhl0nfM8mmh7GS0jg7WrX2Dgq/0Ha/pR1PaR+BvmWwjiQ== dependencies: "@types/yargs-parser" "*" abab@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f" - integrity sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w== - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + version "2.0.2" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.2.tgz#a2fba1b122c69a85caa02d10f9270c7219709a9d" + integrity sha512-2scffjvioEmNz0OyDSLGWDfKCVwaKc6l9Pm9kOIREU13ClXZvHpg/nRL5xyjSSSLhOnXqft2HpsAzNEEA8cFFg== -abbrev@1.0.x: +abbrev@1, abbrev@1.0.x: version "1.0.9" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" integrity sha1-kbR5JYinc4wl813W9jdSovh3YTU= @@ -633,9 +621,9 @@ accepts@~1.3.4: negotiator "0.6.2" acorn-globals@^4.1.0: - version "4.3.3" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.3.tgz#a86f75b69680b8780d30edd21eee4e0ea170c05e" - integrity sha512-vkR40VwS2SYO98AIeFvzWWh+xyc2qi9s7OoXSFEGIP/rOJKzjnhykaZJNnHdoq4BL2gGxI5EZOU16z896EYnOQ== + version "4.3.4" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" + integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A== dependencies: acorn "^6.0.1" acorn-walk "^6.0.1" @@ -655,10 +643,10 @@ acorn@^6.0.1, acorn@^6.0.5: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== -acorn@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.0.0.tgz#26b8d1cd9a9b700350b71c0905546f64d1284e7a" - integrity sha512-PaF/MduxijYYt7unVGRuds1vBC9bFxbNf+VWqhOClfdgy7RlVkQqt610ig1/yxTgsDIfW1cWDel5EBbOy3jdtQ== +acorn@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" + integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== after@0.8.2: version "0.8.2" @@ -680,22 +668,21 @@ agent-base@~4.2.1: es6-promisify "^5.0.0" aggregate-error@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.0.tgz#5b5a3c95e9095f311c9ab16c19fb4f3527cd3f79" - integrity sha512-yKD9kEoJIR+2IFqhMwayIBgheLYbB3PS2OBhWae1L/ODTd/JF/30cW0bc9TqzRL3k4U41Dieu3BF4I29p8xesA== + version "3.0.1" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0" + integrity sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA== dependencies: clean-stack "^2.0.0" - indent-string "^3.2.0" + indent-string "^4.0.0" -ajv-oai@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ajv-oai/-/ajv-oai-1.1.1.tgz#5f3c19ebc6b1628465bc75436c1ade9031b3cab3" - integrity sha1-XzwZ68axYoRlvHVDbBrekDGzyrM= +ajv-oai@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ajv-oai/-/ajv-oai-1.1.5.tgz#f6a3ffa6a84ee90234a296ef733e9e18c875365e" + integrity sha512-JSBfA99K9cI22qJ8rwCyJ7J15iEMFOeHL8LqH+PL47HzeN7YZc7ZazQNg9eLO6TuPLBCfkiuMNLulKTGMzp25w== dependencies: - ajv "^6.1.1" - decimal.js "^9.0.1" + decimal.js "^10.2.0" -ajv@^6.1.1, ajv@^6.5.5, ajv@^6.7: +ajv@^6.10, ajv@^6.5.5: version "6.10.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== @@ -737,6 +724,11 @@ ansi-regex@^4.0.0, ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -749,6 +741,14 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.0.tgz#5681f0dcf7ae5880a7841d8831c4724ed9cc0172" + integrity sha512-7kFQgnEaMdRtwf6uSfUnVr9gSGC7faurn+J/Mv90/W+iTtN0405/nLdopfMWwchyxhbGYl6TC4Sccn9TUkGAgg== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" + ansi-wrap@0.1.0, ansi-wrap@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" @@ -774,10 +774,10 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -anymatch@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.0.3.tgz#2fb624fe0e84bccab00afee3d0006ed310f22f09" - integrity sha512-c6IvoeBECQlMVuYUjSwimnhmztImpErfxJzWZhIQinIvQWoGOnB0dLIgifbPHQt5heS6mNlaZG16f06H3C8t1g== +anymatch@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" @@ -852,23 +852,11 @@ array-slice@^0.2.3: resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" integrity sha1-3Tz7gO15c6dRF82sabC5nshhhvU= -array-union@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= - dependencies: - array-uniq "^1.0.1" - array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= - array-unique@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" @@ -977,15 +965,6 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== -babel-code-frame@^6.22.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - babel-jest@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.9.0.tgz#3fc327cb8467b89d14d7bc70e315104a783ccd54" @@ -1122,9 +1101,14 @@ bluebird@^2.9.33: integrity sha1-U0uQM8AiyVecVro7Plpcqvu2UOE= bluebird@^3.3.0: - version "3.5.5" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f" - integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w== + version "3.7.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.1.tgz#df70e302b471d7473489acf26a93d63b53f874de" + integrity sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg== + +blueimp-md5@^2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.12.0.tgz#be7367938a889dec3ffbb71138617c117e9c130a" + integrity sha512-zo+HIdIhzojv6F1siQPqPFROyVy7C50KzHv/k/Iz+BtvtVzSHXiMXOpq2wCfNkeBqdCv+V8XOV96tsEt2W/3rQ== bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.8" @@ -1180,7 +1164,7 @@ braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1, braces@^3.0.2: +braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -1271,9 +1255,9 @@ bs-logger@0.x: fast-json-stable-stringify "2.x" bser@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.0.tgz#65fc784bf7f87c009b973c12db6546902fa9c7b5" - integrity sha512-8zsjWrQkkBoLK6uxASk1nJ2SKv97ltiGDo6A3wA0/yRPz+CwmEyDo0hUrhIuukG2JHpAl3bvFIixw2/3Hi0DOg== + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== dependencies: node-int64 "^0.4.0" @@ -1306,9 +1290,9 @@ buffer-xor@^1.0.3: integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= buffer@^5.2.1: - version "5.4.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.2.tgz#2012872776206182480eccb2c0fba5f672a2efef" - integrity sha512-iy9koArjAFCzGnx3ZvNA6Z0clIbbFgbdWQ0mKD3hO0krOrZh8UgA6qMKcZvwLJxS+D6iVR76+5/pV56yMNYTag== + version "5.4.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.3.tgz#3fbc9c69eb713d323e3fc1a895eee0710c072115" + integrity sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A== dependencies: base64-js "^1.0.2" ieee754 "^1.1.4" @@ -1338,7 +1322,7 @@ bunyan@^1.8.12: mv "~2" safe-json-stringify "~1" -byline@~5.0.0: +byline@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= @@ -1363,11 +1347,6 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" -call-me-maybe@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" - integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= - caller-callsite@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" @@ -1444,14 +1423,13 @@ chai@^4.1.2: pathval "^1.1.0" type-detect "^4.0.5" -chalk@*, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2, chalk@~2.4.1, chalk@~2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== +chalk@*, chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" + ansi-styles "^4.1.0" + supports-color "^7.1.0" chalk@^1.0.0, chalk@^1.1.3: version "1.1.3" @@ -1464,30 +1442,39 @@ chalk@^1.0.0, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + check-error@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= chokidar@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.0.2.tgz#0d1cd6d04eb2df0327446188cd13736a3367d681" - integrity sha512-c4PR2egjNjI1um6bamCQ6bUNPDiyofNQruHvKgHQ4gDUP/ITSVSzNsiI5OWtHOsX323i5ha/kk4YmOZ1Ktg7KA== - dependencies: - anymatch "^3.0.1" - braces "^3.0.2" - glob-parent "^5.0.0" - is-binary-path "^2.1.0" - is-glob "^4.0.1" - normalize-path "^3.0.0" - readdirp "^3.1.1" + version "3.3.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.0.tgz#12c0714668c55800f659e262d4962a97faf554a6" + integrity sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.2.0" optionalDependencies: - fsevents "^2.0.6" + fsevents "~2.1.1" chownr@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" - integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g== + version "1.1.3" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142" + integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw== ci-info@^2.0.0: version "2.0.0" @@ -1541,6 +1528,15 @@ cliui@^5.0.0: strip-ansi "^5.2.0" wrap-ansi "^5.1.0" +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -1576,15 +1572,27 @@ color-convert@^1.9.0: dependencies: color-name "1.1.3" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + colors@^1.1.0: - version "1.3.3" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d" - integrity sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg== + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== combine-source-map@^0.8.0: version "0.8.0" @@ -1603,10 +1611,10 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.12.1, commander@^2.20.0, commander@~2.20.0: - version "2.20.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" - integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== +commander@^2.12.1, commander@^2.20.0, commander@~2.20.3: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== commondir@^1.0.1: version "1.0.1" @@ -1618,16 +1626,11 @@ component-bind@1.0.0: resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E= -component-emitter@1.2.1: +component-emitter@1.2.1, component-emitter@^1.2.0, component-emitter@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= -component-emitter@^1.2.0, component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - component-inherit@0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" @@ -1649,11 +1652,9 @@ connect@^3.6.0: utils-merge "1.0.1" console-browserify@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" - integrity sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA= - dependencies: - date-now "^0.1.4" + version "1.2.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" + integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" @@ -1670,10 +1671,10 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== -convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" - integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== +convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" + integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== dependencies: safe-buffer "~5.1.1" @@ -1705,14 +1706,14 @@ copyfiles@^2.1.1: yargs "^13.2.4" core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" - integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== + version "2.6.10" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.10.tgz#8a5b8391f8cc7013da703411ce5b585706300d7f" + integrity sha512-I39t74+4t+zau64EN1fE5v2W31Adtc/REhzWN+gWRRXg6WH5qAsZm62DHpQ1+Yhe4047T55jvzz7MUqF/dBBlA== -core-js@^3.1.3, core-js@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.2.1.tgz#cd41f38534da6cc59f7db050fe67307de9868b09" - integrity sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw== +core-js@^3.2.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.4.0.tgz#29ea478601789c72f2978e9bb98f43546f89d3aa" + integrity sha512-lQxb4HScV71YugF/X28LtePZj9AB7WqOpcB+YztYxusvhrgZiQXPmCYfPC5LHsw/+ScEtDbXU3xbqH3CjBRmYA== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -1760,7 +1761,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" -cross-spawn@^6.0.0, cross-spawn@^6.0.5: +cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -1771,6 +1772,15 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14" + integrity sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + crypto-browserify@^3.12.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -1819,12 +1829,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -data-uri-to-buffer@2: - version "2.0.1" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-2.0.1.tgz#ca8f56fe38b1fd329473e9d1b4a9afcd8ce1c045" - integrity sha512-OkVVLrerfAKZlW2ZZ3Ve2y65jgiWqBKsTfUIAFbn8nVbPcCZg6l6gikKlEYv0kXcmzqGm6mFq/Jf2vriuEkv8A== - dependencies: - "@types/node" "^8.0.7" +data-uri-to-buffer@1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835" + integrity sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ== data-urls@^1.0.0: version "1.1.0" @@ -1845,11 +1853,6 @@ date-format@^2.0.0: resolved "https://registry.yarnpkg.com/date-format/-/date-format-2.1.0.tgz#31d5b5ea211cf5fd764cd38baf9d033df7e125cf" integrity sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA== -date-now@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" - integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs= - dateformat@^1.0.6: version "1.0.12" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" @@ -1858,7 +1861,7 @@ dateformat@^1.0.6: get-stdin "^4.0.1" meow "^3.3.0" -debug@2.6.9, debug@^2.2.0, debug@^2.3.3: +debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -1891,10 +1894,10 @@ decamelize@^1.1.2, decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= -decimal.js@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-9.0.1.tgz#1cc8b228177da7ab6498c1cc06eb130a290e6e1e" - integrity sha512-2h0iKbJwnImBk4TGk7CG1xadoA0g3LDPlQhQzbZ221zvG0p2YVUedbKIPsOZXKZGx6YmZMJKYOalpCMxSdDqTQ== +decimal.js@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.0.tgz#39466113a9e036111d02f82489b5fd6b0b5ed231" + integrity sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw== decode-uri-component@^0.2.0: version "0.2.0" @@ -1997,15 +2000,10 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= -dependency-graph@~0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.8.0.tgz#2da2d35ed852ecc24a5d6c17788ba57c3708755b" - integrity sha512-DCvzSq2UiMsuLnj/9AL484ummEgLtZIcRS7YvtO38QnpX3vqh9nJ8P+zhu8Ja+SmLrBHO2iDbva20jq38qvBkQ== - -deprecated-decorator@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz#00966317b7a12fe92f3cc831f7583af329b86c37" - integrity sha1-AJZjF7ehL+kvPMgx91g68ym4bDc= +dependency-graph@^0.8.0, dependency-graph@~0.8.0: + version "0.8.1" + resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.8.1.tgz#9b8cae3aa2c7bd95ccb3347a09a2d1047a6c3c5a" + integrity sha512-g213uqF8fyk40W8SBjm079n3CZB4qSpCrA2ye1fLGzH/4HEgB6tzuW2CbLE7leb4t45/6h44Ud59Su1/ROTfqw== des.js@^1.0.0: version "1.0.0" @@ -2042,11 +2040,6 @@ diff-sequences@^24.9.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== -diff@^3.2.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - diff@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff" @@ -2061,13 +2054,6 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" -dir-glob@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" - integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw== - dependencies: - path-type "^3.0.0" - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -2128,9 +2114,9 @@ elegant-spinner@^1.0.1: integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= elliptic@^6.0.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.0.tgz#2b8ed4c891b7de3200e14412a5b8248c7af505ca" - integrity sha512-eFOJTMyCYb7xtE/caJ6JJu+bhi67WCYNbkGSknu20pmM8Ke/bqOfdnZWxyoGN26JgfxTbXrsCkEw4KheCT/KGg== + version "6.5.1" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.1.tgz#c380f5f909bf1b9b4428d028cd18d3b0efd6b52b" + integrity sha512-xvJINNLbTeWQjrl6X+7eQCrIy/YPv5XCpKW6kB5mKvtnGILoLDcySuwomfdzt0BMdLNVnuRNTuzKNHj0bva1Cg== dependencies: bn.js "^4.4.0" brorand "^1.0.1" @@ -2145,15 +2131,20 @@ emoji-regex@^7.0.1: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= end-of-stream@^1.1.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" - integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" @@ -2207,6 +2198,11 @@ ent@~2.2.0: resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= +eol@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/eol/-/eol-0.9.1.tgz#f701912f504074be35c6117a5c4ade49cd547acd" + integrity sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg== + error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -2215,21 +2211,25 @@ error-ex@^1.2.0, error-ex@^1.3.1: is-arrayish "^0.2.1" es-abstract@^1.12.0, es-abstract@^1.5.1: - version "1.13.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" - integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== + version "1.16.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.0.tgz#d3a26dc9c3283ac9750dca569586e976d9dcc06d" + integrity sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg== dependencies: es-to-primitive "^1.2.0" function-bind "^1.1.1" has "^1.0.3" + has-symbols "^1.0.0" is-callable "^1.1.4" is-regex "^1.0.4" - object-keys "^1.0.12" + object-inspect "^1.6.0" + object-keys "^1.1.1" + string.prototype.trimleft "^2.1.0" + string.prototype.trimright "^2.1.0" es-to-primitive@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" - integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== dependencies: is-callable "^1.1.4" is-date-object "^1.0.1" @@ -2274,7 +2274,7 @@ escodegen@1.8.x: optionalDependencies: source-map "~0.2.0" -escodegen@1.x.x, escodegen@^1.9.1: +escodegen@1.x.x, escodegen@^1.12.0, escodegen@^1.9.1: version "1.12.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.12.0.tgz#f763daf840af172bb3a2b6dd7219c0e17f7ff541" integrity sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg== @@ -2286,18 +2286,6 @@ escodegen@1.x.x, escodegen@^1.9.1: optionalDependencies: source-map "~0.6.1" -escodegen@~1.11.1: - version "1.11.1" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.1.tgz#c485ff8d6b4cdb89e27f4a856e91f118401ca510" - integrity sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw== - dependencies: - esprima "^3.1.3" - estraverse "^4.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - eslint-plugin-prettier@^2.2.0: version "2.7.0" resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-2.7.0.tgz#b4312dcf2c1d965379d7f9d5b5f8aaadc6a45904" @@ -2346,10 +2334,10 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== -eventemitter3@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" - integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== +eventemitter3@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" + integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== events@^3.0.0: version "3.0.0" @@ -2365,9 +2353,9 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: safe-buffer "^5.1.1" exec-sh@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" - integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg== + version "0.3.4" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5" + integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A== execa@^1.0.0: version "1.0.0" @@ -2383,11 +2371,11 @@ execa@^1.0.0: strip-eof "^1.0.0" execa@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/execa/-/execa-2.0.4.tgz#2f5cc589c81db316628627004ea4e37b93391d8e" - integrity sha512-VcQfhuGD51vQUQtKIq2fjGDLDbL6N1DTQVpYzxZ7LPIXw3HqTuIz6uxRmpV1qf8i31LHf2kjiaGI+GdHwRgbnQ== + version "2.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-2.1.0.tgz#e5d3ecd837d2a60ec50f3da78fd39767747bbe99" + integrity sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw== dependencies: - cross-spawn "^6.0.5" + cross-spawn "^7.0.0" get-stream "^5.0.0" is-stream "^2.0.0" merge-stream "^2.0.0" @@ -2429,7 +2417,7 @@ expand-range@^1.8.1: dependencies: fill-range "^2.1.0" -expand-template@~2.0.3: +expand-template@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== @@ -2501,16 +2489,11 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extsprintf@1.3.0: +extsprintf@1.3.0, extsprintf@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= - fast-deep-equal@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" @@ -2521,22 +2504,10 @@ fast-diff@^1.1.1: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== -fast-glob@^2.2.6: - version "2.2.7" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" - integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw== - dependencies: - "@mrmlnc/readdir-enhanced" "^2.2.1" - "@nodelib/fs.stat" "^1.1.2" - glob-parent "^3.1.0" - is-glob "^4.0.0" - merge2 "^1.2.3" - micromatch "^3.1.10" - -fast-glob@^3.0.3, fast-glob@^3.0.4: - version "3.1.0" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.1.0.tgz#77375a7e3e6f6fc9b18f061cddd28b8d1eec75ae" - integrity sha512-TrUz3THiq2Vy3bjfQUB2wNyPdGBeGmdjbzzBLhfHN4YFurYptCKwGq/TfiRavbGywFRzY6U2CdmQ1zmsY5yYaw== +fast-glob@^3.0.3, fast-glob@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.1.1.tgz#87ee30e9e9f3eb40d6f254a7997655da753d7c82" + integrity sha512-nTCREpBY8w8r+boyFYAx21iL6faSsQynliPHM4Uf56SbkyohCNxpVPEH9xrF5TXKy+IsjkPUHDKiUkzBVRXn9g== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -2549,7 +2520,7 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= -fast-levenshtein@~2.0.4: +fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= @@ -2573,10 +2544,10 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fetch-mock@^7.3.9: - version "7.4.0" - resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-7.4.0.tgz#34f2f5cca03de93af9b253ebb8a678b72563cb02" - integrity sha512-q2zrAJh2CcPf6yrDIRxAo18vs1yohETzTNVvtJsqznAAWS4XuJ5M8uaIfrw8odHm7wZJIfMdA6P3RObMNYSWXQ== +fetch-mock@^7.7.3: + version "7.7.3" + resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-7.7.3.tgz#6a3f94cfed6e423ab7f5464912982030da605335" + integrity sha512-I4OkK90JFQnjH8/n3HDtWxH/I6D1wrxoAM2ri+nb444jpuH3RTcgvXx2el+G20KO873W727/66T7QhOvFxNHPg== dependencies: babel-polyfill "^6.26.0" core-js "^2.6.9" @@ -2652,9 +2623,9 @@ finalhandler@1.1.2: unpipe "~1.0.0" find-cache-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.0.0.tgz#cd4b7dd97b7185b7e17dbfe2d6e4115ee3eeb8fc" - integrity sha512-t7ulV1fmbxh5G9l/492O1p5+EBbr3uwpt6odhFTMc+nWyhmbloe+ja9BZ8pIBtqFWhOmCWVjx+pTW4zDkFoclw== + version "3.1.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.1.0.tgz#9935894999debef4cf9f677fdf646d002c4cdecb" + integrity sha512-zw+EFiNBNPgI2NTrKkDd1xd7q0cs6wr/iWnr/oUkI0yF9K9GqQ+riIt4aiyFaaqpaWbxPrJXHI+QvmNUQbX+0Q== dependencies: commondir "^1.0.1" make-dir "^3.0.0" @@ -2675,7 +2646,7 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" -find-up@^4.0.0: +find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== @@ -2699,9 +2670,9 @@ flatted@^2.0.0: integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== follow-redirects@^1.0.0: - version "1.8.1" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.8.1.tgz#24804f9eaab67160b0e840c085885d606371a35b" - integrity sha512-micCIbldHioIegeKs41DoH0KS3AXfFzgS30qVkM6z/XOE/GJgvmsoc839NUqa1B9udYe9dQxgv7KFwng6+p/dw== + version "1.9.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f" + integrity sha512-CRcPzsSIbXyVDl0QI01muNDu69S8trU4jArW9LpOt2WtC6LyUJetcIrmfHsRBx7/Jb6GHJUiuqyYxPooFfNt6A== dependencies: debug "^3.0.0" @@ -2756,7 +2727,7 @@ fs-exists-sync@^0.1.0: resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" integrity sha1-mC1ok6+RjnLQjeyehnP/K1qNat0= -fs-extra@8.1.0: +fs-extra@8.1.0, fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== @@ -2765,7 +2736,7 @@ fs-extra@8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^7.0.1, fs-extra@~7.0.1: +fs-extra@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== @@ -2775,11 +2746,11 @@ fs-extra@^7.0.1, fs-extra@~7.0.1: universalify "^0.1.0" fs-minipass@^1.2.5: - version "1.2.6" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07" - integrity sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ== + version "1.2.7" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" + integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== dependencies: - minipass "^2.2.1" + minipass "^2.6.0" fs.realpath@^1.0.0: version "1.0.0" @@ -2794,10 +2765,10 @@ fsevents@^1.2.7: nan "^2.12.1" node-pre-gyp "^0.12.0" -fsevents@^2.0.6: - version "2.0.7" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.0.7.tgz#382c9b443c6cbac4c57187cdda23aa3bf1ccfc2a" - integrity sha512-a7YT0SV3RB+DjYcppwVDLtn13UQnmg0SWZS7ezZD0UjnLwXmy8Zm21GMVGLaFGimIqcvyMQaOJBrop8MyOp1kQ== +fsevents@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" + integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA== ftp@~0.3.10: version "0.3.10" @@ -2837,9 +2808,9 @@ get-func-name@^2.0.0: integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= get-own-enumerable-property-symbols@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz#b877b49a5c16aefac3655f2ed2ea5b684df8d203" - integrity sha512-CIJYJC4GGF06TakLg8z4GQKvDsx9EMspVxOYih7LerEL/WosUnFIww45CGfxfeKHqlg3twgUrYRT1O3WQqjGCg== + version "3.0.1" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.1.tgz#6f7764f88ea11e0b514bd9bd860a132259992ca4" + integrity sha512-09/VS4iek66Dh2bctjRkowueRJbY1JDGR1L/zRxO1Qk8Uxs6PnqaNSqalpizPT+CDjre3hnEsuzvhgomz9qYrA== get-stdin@^4.0.1: version "4.0.1" @@ -2866,16 +2837,16 @@ get-stream@^5.0.0: pump "^3.0.0" get-uri@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-2.0.3.tgz#fa13352269781d75162c6fc813c9e905323fbab5" - integrity sha512-x5j6Ks7FOgLD/GlvjKwgu7wdmMR55iuRHhn8hj/+gA+eSbxQvZ+AEomq+3MgVEZj1vpi738QahGbCCSIDtXtkw== + version "2.0.4" + resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-2.0.4.tgz#d4937ab819e218d4cb5ae18e4f5962bef169cc6a" + integrity sha512-v7LT/s8kVjs+Tx0ykk1I+H/rbpzkHvuIq87LmeXptcf5sNWm9uQiwjNAt94SJPA1zOlCntmnOlJvVWKmzsxG8Q== dependencies: - data-uri-to-buffer "2" - debug "4" + data-uri-to-buffer "1" + debug "2" extend "~3.0.2" file-uri-to-path "1" ftp "~0.3.10" - readable-stream "3" + readable-stream "2" get-value@^1.1.5: version "1.3.1" @@ -2967,26 +2938,13 @@ glob-parent@^2.0.0: dependencies: is-glob "^2.0.0" -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob-parent@^5.0.0, glob-parent@^5.1.0: +glob-parent@^5.1.0, glob-parent@~5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw== dependencies: is-glob "^4.0.1" -glob-to-regexp@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" - integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= - glob-to-regexp@^0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" @@ -3015,9 +2973,9 @@ glob@^6.0.1: path-is-absolute "^1.0.0" glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@~7.1.4: - version "7.1.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" - integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -3063,24 +3021,10 @@ globby@^10.0.1: merge2 "^1.2.3" slash "^3.0.0" -globby@~9.2.0: - version "9.2.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d" - integrity sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg== - dependencies: - "@types/glob" "^7.1.1" - array-union "^1.0.2" - dir-glob "^2.2.2" - fast-glob "^2.2.6" - glob "^7.1.3" - ignore "^4.0.3" - pify "^4.0.1" - slash "^2.0.0" - graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02" - integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q== + version "4.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" + integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== grapheme-splitter@^1.0.4: version "1.0.4" @@ -3093,9 +3037,9 @@ growly@^1.3.0: integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= handlebars@^4.0.1, handlebars@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" - integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw== + version "4.5.1" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.1.tgz#8a01c382c180272260d07f2d1aa3ae745715c7ba" + integrity sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA== dependencies: neo-async "^2.6.0" optimist "^0.6.1" @@ -3145,6 +3089,11 @@ has-flag@^3.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + has-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" @@ -3209,6 +3158,11 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +he@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -3226,9 +3180,9 @@ homedir-polyfill@^1.0.0: parse-passwd "^1.0.0" hosted-git-info@^2.1.4: - version "2.8.4" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.4.tgz#44119abaf4bc64692a16ace34700fed9c03e2546" - integrity sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ== + version "2.8.5" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c" + integrity sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg== html-encoding-sniffer@^1.0.2: version "1.0.2" @@ -3248,17 +3202,6 @@ http-errors@1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-errors@1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - http-proxy-agent@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" @@ -3268,11 +3211,11 @@ http-proxy-agent@^2.1.0: debug "3.1.0" http-proxy@^1.13.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" - integrity sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g== + version "1.18.0" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a" + integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ== dependencies: - eventemitter3 "^3.0.0" + eventemitter3 "^4.0.0" follow-redirects "^1.0.0" requires-port "^1.0.0" @@ -3290,28 +3233,28 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.2.tgz#271ea8e90f836ac9f119daccd39c19ff7dfb0793" - integrity sha512-c8Ndjc9Bkpfx/vCJueCPy0jlP4ccCCSNDp8xwCZzPjKJUm+B+u9WX2x98Qx4n1PiMNTWo3D7KK5ifNV/yJyRzg== +https-proxy-agent@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81" + integrity sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg== dependencies: agent-base "^4.3.0" debug "^3.1.0" -husky@^3.0.0: - version "3.0.8" - resolved "https://registry.yarnpkg.com/husky/-/husky-3.0.8.tgz#8de3fed26ce9b43034ef51013c4ad368b6b74ea8" - integrity sha512-HFOsgcyrX3qe/rBuqyTt+P4Gxn5P0seJmr215LAZ/vnwK3jWB3r0ck7swbzGRUbufCf9w/lgHPVbF/YXQALgfQ== +husky@^3.0.9: + version "3.1.0" + resolved "https://registry.yarnpkg.com/husky/-/husky-3.1.0.tgz#5faad520ab860582ed94f0c1a77f0f04c90b57c0" + integrity sha512-FJkPoHHB+6s4a+jwPqBudBDvYZsoQW5/HBuMSehC8qDiCe50kpcxeqFoDSlow+9I6wg47YxBoT3WxaURlrDIIQ== dependencies: chalk "^2.4.2" + ci-info "^2.0.0" cosmiconfig "^5.2.1" execa "^1.0.0" get-stdin "^7.0.0" - is-ci "^2.0.0" opencollective-postinstall "^2.0.2" pkg-dir "^4.2.0" please-upgrade-node "^3.2.0" - read-pkg "^5.1.1" + read-pkg "^5.2.0" run-node "^1.0.0" slash "^3.0.0" @@ -3328,26 +3271,21 @@ ieee754@^1.1.4: integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== ignore-walk@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" - integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ== + version "3.0.3" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" + integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== dependencies: minimatch "^3.0.4" -ignore@^4.0.3: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - ignore@^5.1.1: version "5.1.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== -immer@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/immer/-/immer-3.2.0.tgz#53686471e9dd2b070e0fb5500c6fdecd3a99375f" - integrity sha512-+a2R8z9eELHst6aht++nzVzJ8LJ+Hsg49qttfg9Kc/vmoxEdPXw5/rV6+4DYWGgnq+B36KbLr4OTaGtS9mDjtg== +immer@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/immer/-/immer-4.0.2.tgz#9ff0fcdf88e06f92618a5978ceecb5884e633559" + integrity sha512-Q/tm+yKqnKy4RIBmmtISBlhXuSDrB69e9EKTYiIenIKQkXBQir43w+kN/eGiax3wt1J0O1b2fYcNqLSbEcXA7w== import-fresh@^2.0.0: version "2.0.0" @@ -3377,11 +3315,16 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" -indent-string@^3.0.0, indent-string@^3.2.0: +indent-string@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + indexof@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" @@ -3395,7 +3338,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -3417,13 +3360,13 @@ inline-source-map@~0.6.0: dependencies: source-map "~0.5.3" -into-stream@~5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-5.1.0.tgz#b05f37d8fed05c06a0b43b556d74e53e5af23878" - integrity sha512-cbDhb8qlxKMxPBk/QxTtYg1DQ4CwXmadu7quG3B7nrJsgSncEreF2kwWKZFdnjc/lSNNIkFPsjI7SM0Cx/QXPw== +into-stream@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-5.1.1.tgz#f9a20a348a11f3c13face22763f2d02e127f4db8" + integrity sha512-krrAJ7McQxGGmvaYbB7Q1mcA+cRwg9Ij2RfWIeVesNBgVDZmzY/Fa4IpZUT3bmdRzMzdf/mzltCG2Dq99IZGBA== dependencies: from2 "^2.3.0" - p-is-promise "^2.0.0" + p-is-promise "^3.0.0" invariant@^2.2.4: version "2.2.4" @@ -3432,7 +3375,7 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -ip@^1.1.5: +ip@1.1.5, ip@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= @@ -3469,7 +3412,7 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= -is-binary-path@^2.1.0: +is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== @@ -3571,7 +3514,7 @@ is-extglob@^1.0.0: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" integrity sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA= -is-extglob@^2.1.0, is-extglob@^2.1.1: +is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= @@ -3595,6 +3538,11 @@ is-fullwidth-code-point@^2.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-generator-fn@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" @@ -3617,14 +3565,7 @@ is-glob@^2.0.0, is-glob@^2.0.1: dependencies: is-extglob "^1.0.0" -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - -is-glob@^4.0.0, is-glob@^4.0.1: +is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== @@ -3685,9 +3626,9 @@ is-path-cwd@^2.2.0: integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== is-path-inside@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.1.tgz#7417049ed551d053ab82bba3fdd6baa6b3a81e89" - integrity sha512-CKstxrctq1kUesU6WhtZDbYKzzYBuRH0UYInAVrkc/EYdB9ltbfE0gOoayG9nhohG6447sOOVGhHqsdmBvkbNg== + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" + integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" @@ -3712,9 +3653,9 @@ is-promise@^2.1.0: integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= is-reference@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.1.3.tgz#e99059204b66fdbe09305cfca715a29caa5c8a51" - integrity sha512-W1iHHv/oyBb2pPxkBxtaewxa1BC58Pn5J0hogyCdefwUIvb6R+TGbAcIa4qPNYLqLhb3EnOgUf2MQkkF76BcKw== + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.1.4.tgz#3f95849886ddb70256a3e6d062b1a68c13c51427" + integrity sha512-uJA/CDPO3Tao3GTrxYn6AwkM4nUPJiGGYu5+cB8qbC7WGFlrKZbiRo7SFKxUAEpFUfiHofWCXBUNhvYJMh+6zw== dependencies: "@types/estree" "0.0.39" @@ -3906,9 +3847,9 @@ istanbul@0.4.5, istanbul@^0.4.0: wordwrap "^1.0.0" jasmine-core@^3.3: - version "3.4.0" - resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.4.0.tgz#2a74618e966026530c3518f03e9f845d26473ce3" - integrity sha512-HU/YxV4i6GcmiH4duATwAbJQMlE0MsDIR5XmSVxURxKHn3aGAdbY1/ZJFmVRbKtnLwIxxMJD7gYaPsypcbYimg== + version "3.5.0" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.5.0.tgz#132c23e645af96d85c8bca13c8758b18429fc1e4" + integrity sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA== jest-changed-files@^24.9.0: version "24.9.0" @@ -3961,7 +3902,7 @@ jest-config@^24.9.0: pretty-format "^24.9.0" realpath-native "^1.1.0" -jest-diff@^24.9.0: +jest-diff@^24.3.0, jest-diff@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da" integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ== @@ -4095,7 +4036,7 @@ jest-message-util@^24.9.0: slash "^2.0.0" stack-utils "^1.0.1" -jest-mock@^24.8.0, jest-mock@^24.9.0: +jest-mock@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.9.0.tgz#c22835541ee379b908673ad51087a2185c13f1c6" integrity sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w== @@ -4282,12 +4223,7 @@ jest@~24.9: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= - -js-yaml@3.x, js-yaml@^3.13.1, js-yaml@^3.7.0: +js-yaml@3.x, js-yaml@^3.13.1: version "3.13.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== @@ -4373,9 +4309,9 @@ json-to-ast@^2.0.3: grapheme-splitter "^1.0.4" json5@2.x, json5@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850" - integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ== + version "2.1.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6" + integrity sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ== dependencies: minimist "^1.2.0" @@ -4386,10 +4322,10 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -jsonc-parser@~2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.1.1.tgz#83dc3d7a6e7186346b889b1280eefa04446c6d3e" - integrity sha512-VC0CjnWJylKB1iov4u76/W/5Ef0ydDkjtYWxoZ9t3HdWlSnZQwZL5MgFikaB/EtQ4RmMEw3tmQzuYnZA2/Ja1g== +jsonc-parser@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.2.0.tgz#f206f87f9d49d644b7502052c04e82dd6392e9ef" + integrity sha512-4fLQxW1j/5fWj6p78vAlAafoCKtuBm6ghv+Ij5W2DrDx0qE+ZdEl2c6Ko1mgJNF5ftX1iEWQQ4Ap7+3GlhjkOA== jsonfile@^4.0.0: version "4.0.0" @@ -4403,10 +4339,10 @@ jsonify@~0.0.0: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= -jsonpath-plus@~1.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/jsonpath-plus/-/jsonpath-plus-1.0.0.tgz#78fa5c4ae62968476268d505d9f78c65469d0e7c" - integrity sha512-CXQJ/tsgFogKYBuCRmnlChIw66JBXp8kAkT+R4mSB2cuzCSBi88lx2A+vHvo27RY4Wtj5xVVGu2/2O7NwZ79mg== +jsonpath-plus@~2.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jsonpath-plus/-/jsonpath-plus-2.0.0.tgz#3ebf30b79e2262d69826084ba03c406a9de440a0" + integrity sha512-ksXaz9+3SIZ5BMxgr7MQueYcR515VRZPuoDhIymUd1JcF6BnVaYJS7k4NJni4EHhvJaOIGGiPqT8+ifsGp6mBw== jsonpointer@^4.0.1: version "4.0.1" @@ -4423,7 +4359,7 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -karma-chrome-launcher@^3.0.0: +karma-chrome-launcher@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz#805a586799a4d05f4e54f72a204979f3f3066738" integrity sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg== @@ -4495,10 +4431,10 @@ karma-typescript@^4.1.1: util "^0.12.0" vm-browserify "1.1.0" -karma@^4.2.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/karma/-/karma-4.3.0.tgz#e14471ea090a952265a42ebb442b1a3c09832559" - integrity sha512-NSPViHOt+RW38oJklvYxQC4BSQsv737oQlr/r06pCM+slDOr4myuI1ivkRmp+3dVpJDfZt2DmaPJ2wkx+ZZuMQ== +karma@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/karma/-/karma-4.4.1.tgz#6d9aaab037a31136dc074002620ee11e8c2e32ab" + integrity sha512-L5SIaXEYqzrh6b1wqYC42tNsFMx2PWuxky84pK9coK09MvmL7mxii3G3bZBh/0rvD27lqDd0le9jyhzvwif73A== dependencies: bluebird "^3.3.0" body-parser "^1.16.1" @@ -4506,7 +4442,6 @@ karma@^4.2.0: chokidar "^3.0.0" colors "^1.1.0" connect "^3.6.0" - core-js "^3.1.3" di "^0.0.1" dom-serialize "^2.2.0" flatted "^2.0.0" @@ -4609,10 +4544,10 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -lint-staged@^9.0.2: - version "9.4.1" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-9.4.1.tgz#60c0f85745bd398e6460aa7f5adb3cad3a2b862c" - integrity sha512-zFRbo1bAJEVf1m33paTTjDVfy2v3lICCqHfmQSgNoI+lWpi7HPG5y/R2Y7Whdce+FKxlZYs/U1sDSx8+nmQdDA== +lint-staged@^9.4.2: + version "9.5.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-9.5.0.tgz#290ec605252af646d9b74d73a0fa118362b05a33" + integrity sha512-nawMob9cb/G1J98nb8v3VC/E8rcX1rryUYXVZ69aT9kde6YWX+uvNOEHY5yf2gcWcTJGiD0kqXmCnS3oD75GIA== dependencies: chalk "^2.4.2" commander "^2.20.0" @@ -4783,7 +4718,7 @@ loud-rejection@^1.0.0: currently-unhandled "^0.4.1" signal-exit "^3.0.0" -lru-cache@4.1.x, lru-cache@^4.1.2: +lru-cache@4.1.x: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== @@ -4791,10 +4726,17 @@ lru-cache@4.1.x, lru-cache@^4.1.2: pseudomap "^1.0.2" yallist "^2.1.2" +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + magic-string@^0.25.2: - version "0.25.3" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.3.tgz#34b8d2a2c7fec9d9bdf9929a3fd81d271ef35be9" - integrity sha512-6QK0OpF/phMz0Q2AxILkX2mFhi7m+WMwTRg0LQKq/WBB0cDP4rYH3Wp4/d3OTXlrPLVJT/RFqj8tFeAR4nk8AA== + version "0.25.4" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.4.tgz#325b8a0a79fc423db109b77fd5a19183b7ba5143" + integrity sha512-oycWO9nEVAP2RVPbIoDoA4Y7LFIJ3xRYov93gAyJhZkET1tNuB0u7uWkZS2LpBWTJUWnmau/To8ECWRC+jKNfw== dependencies: sourcemap-codec "^1.4.4" @@ -5001,35 +4943,30 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: dependencies: brace-expansion "^1.1.7" -minimist@0.0.8: +minimist@0.0.8, minimist@~0.0.1: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0: +minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= - -minipass@^2.2.1, minipass@^2.3.5: - version "2.4.0" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.4.0.tgz#38f0af94f42fb6f34d3d7d82a90e2c99cd3ff485" - integrity sha512-6PmOuSP4NnZXzs2z6rbwzLJu/c5gdzYg1mRI/WIYdx45iiX7T+a4esOzavD6V/KmBzAaopFSTZPZcUx73bqKWA== +minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" + integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== dependencies: safe-buffer "^5.1.2" yallist "^3.0.0" minizlib@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" - integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== + version "1.3.3" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" + integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== dependencies: - minipass "^2.2.1" + minipass "^2.9.0" mixin-deep@^1.2.0: version "1.3.2" @@ -5069,7 +5006,7 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -multistream@~2.1.1: +multistream@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/multistream/-/multistream-2.1.1.tgz#629d3a29bd76623489980d04519a2c365948148c" integrity sha512-xasv76hl6nr1dEy3lPvy7Ej7K/Lx3O/FCvwge8PeVJpciPPoNCbaANcNiBug3IpdvTveZUcAV0DJzdnUDMesNQ== @@ -5091,10 +5028,10 @@ nan@^2.12.1, nan@^2.14.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== -nanoid@^2.0.3: - version "2.1.2" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.2.tgz#5c63a913f4fbf4afff2004c7bb42dee8e627baf4" - integrity sha512-q0iKJHcLc9rZg/qtJ/ioG5s6/5357bqvkYCpqXJxpcyfK7L5us8+uJllZosqPWou7l6E1lY2Qqoq5ce+AMbFuQ== +nanoid@^2.1.6: + version "2.1.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.8.tgz#2dbb0224231b246e3b4c819de7bfea6384dabf08" + integrity sha512-g1z+n5s26w0TGKh7gjn7HCqurNKMZWzH08elXzh/gM/csQHd/UqDV6uxMghQYg9IvqRPm1QpeMk50YMofHvEjQ== nanomatch@^1.2.9: version "1.2.13" @@ -5152,10 +5089,10 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -nock@*, nock@~11.4.0: - version "11.4.0" - resolved "https://registry.yarnpkg.com/nock/-/nock-11.4.0.tgz#68c2f9cf533f13ecb1a43e956823dbfba5593d17" - integrity sha512-UrVEbEAvhyDoUttrS0fv3znhZ5nEJvlxqgmrC6Gb2Mf9cFci65RMK17e6EjDDQB57g5iwZw1TFnVvyeL0eUlhQ== +nock@*, nock@~11.7.0: + version "11.7.0" + resolved "https://registry.yarnpkg.com/nock/-/nock-11.7.0.tgz#5eaae8b8a55c0dfc014d05692c8cf3d31d61a342" + integrity sha512-7c1jhHew74C33OBeRYyQENT+YXQiejpwIrEjinh6dRurBae+Ei4QjeUaPlkptIF0ZacEiVCnw8dWaxqepkiihg== dependencies: chai "^4.1.2" debug "^4.1.0" @@ -5164,11 +5101,18 @@ nock@*, nock@~11.4.0: mkdirp "^0.5.0" propagate "^2.0.0" -node-fetch@^2.6: +node-fetch@^2.6, node-fetch@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== +node-html-parser@^1.1.16: + version "1.1.16" + resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-1.1.16.tgz#59f072bcabf1cf6e23f7878a76e0c3b81b9194fa" + integrity sha512-cfqTZIYDdp5cGh3NvCD5dcEDP7hfyni7WgyFacmDynLlIZaF3GVlRk8yMARhWp/PobWt1KaCV8VKdP5LKWiVbg== + dependencies: + he "1.1.1" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -5251,7 +5195,7 @@ normalize-path@^2.0.1, normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" -normalize-path@^3.0.0: +normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== @@ -5262,9 +5206,9 @@ npm-bundled@^1.0.1: integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== npm-packlist@^1.1.6: - version "1.4.4" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.4.tgz#866224233850ac534b63d1a6e76050092b5d2f44" - integrity sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw== + version "1.4.6" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.6.tgz#53ba3ed11f8523079f1457376dd379ee4ea42ff4" + integrity sha512-u65uQdb+qwtGvEJh/DgQgW1Xg7sqeNbmxYyrvlNznaVTjV3E5P6F/EFjM+BVHXl7JJlsdG8A64M0XI8FI/IOlg== dependencies: ignore-walk "^3.0.1" npm-bundled "^1.0.1" @@ -5299,9 +5243,9 @@ number-is-nan@^1.0.0: integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= nwsapi@^2.0.7: - version "2.1.4" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.4.tgz#e006a878db23636f8e8a67d33ca0e4edf61a842f" - integrity sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw== + version "2.2.0" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" + integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== oauth-sign@~0.9.0: version "0.9.0" @@ -5327,12 +5271,17 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-inspect@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" + integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ== + object-is@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6" integrity sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY= -object-keys@^1.0.12: +object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== @@ -5434,16 +5383,16 @@ optimist@^0.6.1: wordwrap "~0.0.2" optionator@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" - integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== dependencies: deep-is "~0.1.3" - fast-levenshtein "~2.0.4" + fast-levenshtein "~2.0.6" levn "~0.3.0" prelude-ls "~1.1.2" type-check "~0.3.2" - wordwrap "~1.0.0" + word-wrap "~1.2.3" os-browserify@^0.3.0: version "0.3.0" @@ -5485,10 +5434,10 @@ p-finally@^2.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== -p-is-promise@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" - integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== +p-is-promise@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-3.0.0.tgz#58e78c7dfe2e163cf2a04ff869e7c1dba64a5971" + integrity sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ== p-limit@^2.0.0, p-limit@^2.2.0: version "2.2.1" @@ -5533,16 +5482,16 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -pac-proxy-agent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-3.0.0.tgz#11d578b72a164ad74bf9d5bac9ff462a38282432" - integrity sha512-AOUX9jES/EkQX2zRz0AW7lSx9jD//hQS8wFXBvcnd/J2Py9KaMJMqV/LPqJssj1tgGufotb2mmopGPR15ODv1Q== +pac-proxy-agent@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-3.0.1.tgz#115b1e58f92576cac2eba718593ca7b0e37de2ad" + integrity sha512-44DUg21G/liUZ48dJpUSjZnFfZro/0K5JTyFYLBcmh9+T6Ooi4/i4efwUiEy0+4oQusCBqWdhv16XohIj1GqnQ== dependencies: agent-base "^4.2.0" - debug "^3.1.0" + debug "^4.1.1" get-uri "^2.0.0" http-proxy-agent "^2.1.0" - https-proxy-agent "^2.2.1" + https-proxy-agent "^3.0.0" pac-resolver "^3.0.0" raw-body "^2.2.0" socks-proxy-agent "^4.0.1" @@ -5571,9 +5520,9 @@ pako@~1.0.5: integrity sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw== parse-asn1@^5.0.0: - version "5.1.4" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.4.tgz#37f6628f823fbdeb2273b4d540434a22f3ef1fcc" - integrity sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw== + version "5.1.5" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.5.tgz#003271343da58dc94cace494faef3d2147ecea0e" + integrity sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ== dependencies: asn1.js "^4.0.0" browserify-aes "^1.0.0" @@ -5673,11 +5622,6 @@ path-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.0.tgz#40702a97af46ae00b0ea6fa8998c0b03c0af160d" integrity sha512-Hkavx/nY4/plImrZPHRk2CL9vpOymZLgEbMNX1U0bjcBL7QN9wODxyx0yaMZURSQaUtSEvDrfAvxa9oPb0at9g== -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - path-exists@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" @@ -5705,12 +5649,12 @@ path-key@^2.0.0, path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= -path-key@^3.0.0: +path-key@^3.0.0, path-key@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.0.tgz#99a10d870a803bdd5ee6f0470e58dfcd2f9a54d3" integrity sha512-8cChqz0RP6SHJkMt48FW0A7+qUOn+OsnOsVtzI59tZ8m+5bCSk7hzwET0pulwOM2YMn9J1efb07KB9l9f30SGg== -path-parse@^1.0.5, path-parse@^1.0.6: +path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== @@ -5763,9 +5707,9 @@ performance-now@^2.1.0: integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= picomatch@^2.0.4, picomatch@^2.0.5: - version "2.0.7" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" - integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== + version "2.1.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.1.1.tgz#ecdfbea7704adb5fe6fb47f9866c4c0e15e905c5" + integrity sha512-OYMyqkKzK7blWO/+XZYP6w8hH0LDvkBvdvKukti+7kqYFCiEAk+gI3DWnryapc0Dau05ugGTy0foQ6mqn4AHYA== pify@^2.0.0: version "2.3.0" @@ -5815,41 +5759,41 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -pkg-fetch@~2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/pkg-fetch/-/pkg-fetch-2.6.2.tgz#bad65a1f77f3bbd371be332a8da05a1d0c7edc7c" - integrity sha512-7DN6YYP1Kct02mSkhfblK0HkunJ7BJjGBkSkFdIW/QKIovtAMaICidS7feX+mHfnZ98OP7xFJvBluVURlrHJxA== - dependencies: - "@babel/runtime" "~7.4.4" - byline "~5.0.0" - chalk "~2.4.1" - expand-template "~2.0.3" - fs-extra "~7.0.1" - minimist "~1.2.0" - progress "~2.0.0" - request "~2.88.0" - request-progress "~3.0.0" - semver "~6.0.0" - unique-temp-dir "~1.0.0" +pkg-fetch@^2.6.4: + version "2.6.4" + resolved "https://registry.yarnpkg.com/pkg-fetch/-/pkg-fetch-2.6.4.tgz#0faac4c4ae9668e1daf9819180606966a5e33f96" + integrity sha512-4j4jiuo6RRIuD9e9xUE6OQYnIkQCArZjkHXNYsSJjxhJeiHE16MA+rENMblvGLbeWsTY3BPfcYVCGFXzpfJetA== + dependencies: + "@babel/runtime" "^7.7.5" + byline "^5.0.0" + chalk "^3.0.0" + expand-template "^2.0.3" + fs-extra "^8.1.0" + minimist "^1.2.0" + progress "^2.0.3" + request "^2.88.0" + request-progress "^3.0.0" + semver "^6.3.0" + unique-temp-dir "^1.0.0" pkg@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/pkg/-/pkg-4.4.0.tgz#9b6f2c77f891b6eb403681f5a8c1d2de09a976d3" - integrity sha512-bFNJ3v56QwqB6JtAl/YrczlmEKBPBVJ3n5nW905kgvG1ex9DajODpTs0kLAFxyLwoubDQux/RPJFL6WrnD/vpg== - dependencies: - "@babel/parser" "~7.4.4" - "@babel/runtime" "~7.4.4" - chalk "~2.4.2" - escodegen "~1.11.1" - fs-extra "~7.0.1" - globby "~9.2.0" - into-stream "~5.1.0" - minimist "~1.2.0" - multistream "~2.1.1" - pkg-fetch "~2.6.2" - progress "~2.0.3" - resolve "1.6.0" - stream-meter "~1.0.4" + version "4.4.2" + resolved "https://registry.yarnpkg.com/pkg/-/pkg-4.4.2.tgz#c9c5bf13501d33df5cad02397a14f0bbf48aac15" + integrity sha512-FEFX43fzHVyEl7fBTTaKxjN3OsWowNfcDGO7+NaxfUsMTMvy8aQX6DscjgoTNnbOehObRK/UqMUGKXt3mvnArg== + dependencies: + "@babel/parser" "^7.7.5" + "@babel/runtime" "^7.7.5" + chalk "^3.0.0" + escodegen "^1.12.0" + fs-extra "^8.1.0" + globby "^10.0.1" + into-stream "^5.1.1" + minimist "^1.2.0" + multistream "^2.1.1" + pkg-fetch "^2.6.4" + progress "^2.0.3" + resolve "^1.13.1" + stream-meter "^1.0.4" please-upgrade-node@^3.1.1, please-upgrade-node@^3.2.0: version "3.2.0" @@ -5888,10 +5832,10 @@ preserve@^0.2.0: resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= -prettier@1.14.x: - version "1.14.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.3.tgz#90238dd4c0684b7edce5f83b0fb7328e48bd0895" - integrity sha512-qZDVnCrnpsRJJq5nSsiHCE3BYMED2OtsI+cmzIzF1QIfqm5ALf8tEJcO27zV1gKNKRPdhjO0dNWnrzssDQ1tFg== +prettier@^1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" + integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== pretty-format@^24.9.0: version "24.9.0" @@ -5918,7 +5862,7 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= -progress@~2.0.0, progress@~2.0.3: +progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -5936,17 +5880,17 @@ propagate@^2.0.0: resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== -proxy-agent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-3.1.0.tgz#3cf86ee911c94874de4359f37efd9de25157c113" - integrity sha512-IkbZL4ClW3wwBL/ABFD2zJ8iP84CY0uKMvBPk/OceQe/cEjrxzN1pMHsLwhbzUoRhG9QbSxYC+Z7LBkTiBNvrA== +proxy-agent@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-3.1.1.tgz#7e04e06bf36afa624a1540be247b47c970bd3014" + integrity sha512-WudaR0eTsDx33O3EJE16PjBRZWcX8GqCEeERw1W3hZJgH/F2a46g7jty6UGty6NeJ4CKQy8ds2CJPMiyeqaTvw== dependencies: agent-base "^4.2.0" - debug "^3.1.0" + debug "4" http-proxy-agent "^2.1.0" - https-proxy-agent "^2.2.1" - lru-cache "^4.1.2" - pac-proxy-agent "^3.0.0" + https-proxy-agent "^3.0.0" + lru-cache "^5.1.1" + pac-proxy-agent "^3.0.1" proxy-from-env "^1.0.0" socks-proxy-agent "^4.0.1" @@ -5960,10 +5904,10 @@ pseudomap@^1.0.2: resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= -psl@^1.1.24, psl@^1.1.28: - version "1.3.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.3.0.tgz#e1ebf6a3b5564fa8376f3da2275da76d875ca1bd" - integrity sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag== +psl@^1.1.24: + version "1.4.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2" + integrity sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw== public-encrypt@^4.0.0: version "4.0.3" @@ -6054,7 +5998,7 @@ range-parser@^1.2.0: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.4.0: +raw-body@2.4.0, raw-body@^2.2.0: version "2.4.0" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== @@ -6064,16 +6008,6 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" -raw-body@^2.2.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" - integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA== - dependencies: - bytes "3.1.0" - http-errors "1.7.3" - iconv-lite "0.4.24" - unpipe "1.0.0" - rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -6085,9 +6019,9 @@ rc@^1.2.7: strip-json-comments "~2.0.1" react-is@^16.8.4: - version "16.9.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb" - integrity sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw== + version "16.11.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.11.0.tgz#b85dfecd48ad1ce469ff558a882ca8e8313928fa" + integrity sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw== read-pkg-up@^1.0.1: version "1.0.1" @@ -6123,7 +6057,7 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" -read-pkg@^5.1.1: +read-pkg@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== @@ -6143,16 +6077,7 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -"readable-stream@2 || 3", readable-stream@3, readable-stream@^3.0.6, readable-stream@^3.1.1: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" - integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@~2.3.6: +readable-stream@2, "readable-stream@2 || 3", readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== @@ -6165,6 +6090,15 @@ readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^3.0.6, readable-stream@^3.1.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" + integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-stream@~1.0.31: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" @@ -6175,10 +6109,10 @@ readable-stream@~1.0.31: isarray "0.0.1" string_decoder "~0.10.x" -readdirp@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.1.2.tgz#fa85d2d14d4289920e4671dead96431add2ee78a" - integrity sha512-8rhl0xs2cxfVsqzreYCvs8EwBfn/DhVdqtoLmw19uI3SC5avYX9teCurlErfpPXGmYtMHReGaP2RsLnFvz/lnw== +readdirp@~3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839" + integrity sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ== dependencies: picomatch "^2.0.4" @@ -6189,10 +6123,10 @@ realpath-native@^1.1.0: dependencies: util.promisify "^1.0.0" -recast@^0.18.1: - version "0.18.2" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.18.2.tgz#ada263677edc70c45408caf20e6ae990958fdea8" - integrity sha512-MbuHc1lzIDIn7bpxaqIAGwwtyaokkzPqINf1Vm/LA0BSyVrTgXNVTTT7RzWC9kP+vqrUoYVpd6wHhI8x75ej8w== +recast@^0.18.5: + version "0.18.5" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.18.5.tgz#9d5adbc07983a3c8145f3034812374a493e0fe4d" + integrity sha512-sD1WJrpLQAkXGyQZyGzTM75WJvyAd98II5CHdK3IYbt/cZlU0UzCRVU11nUFNXX9fBVEt4E9ajkMjBlUlG+Oog== dependencies: ast-types "0.13.2" esprima "~4.0.0" @@ -6277,30 +6211,30 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request-progress@~3.0.0: +request-progress@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4= dependencies: throttleit "^1.0.0" -request-promise-core@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346" - integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag== +request-promise-core@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9" + integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ== dependencies: - lodash "^4.17.11" + lodash "^4.17.15" request-promise-native@^1.0.5: - version "1.0.7" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" - integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== + version "1.0.8" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36" + integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ== dependencies: - request-promise-core "1.1.2" + request-promise-core "1.1.3" stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request@^2.87.0, request@~2.88.0: +request@^2.87.0, request@^2.88.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== @@ -6371,19 +6305,19 @@ resolve@1.1.7, resolve@1.1.x: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@1.12.0, resolve@1.x, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.11.1, resolve@^1.3.2: +resolve@1.12.0: version "1.12.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== dependencies: path-parse "^1.0.6" -resolve@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.6.0.tgz#0fbd21278b27b4004481c395349e7aba60a9ff5c" - integrity sha512-mw7JQNu5ExIkcw4LPih0owX/TZXjD/ZUF/ZQ/pDnkw3ZKhDcZZw5klmBlj6gVMwjQ3Pz5Jgu7F3d0jcDVuEWdw== +resolve@1.x, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.11.1, resolve@^1.13.1, resolve@^1.3.2: + version "1.13.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.13.1.tgz#be0aa4c06acd53083505abb35f4d66932ab35d16" + integrity sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w== dependencies: - path-parse "^1.0.5" + path-parse "^1.0.6" restore-cursor@^2.0.0: version "2.0.0" @@ -6437,7 +6371,7 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rollup-plugin-commonjs@^10.0.2: +rollup-plugin-commonjs@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz#417af3b54503878e084d127adf4d1caf8beb86fb" integrity sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q== @@ -6459,21 +6393,21 @@ rollup-plugin-node-resolve@^5.2.0: resolve "^1.11.1" rollup-pluginutils "^2.8.1" -rollup-plugin-terser@^5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-5.1.2.tgz#3e41256205cb75f196fc70d4634227d1002c255c" - integrity sha512-sWKBCOS+vUkRtHtEiJPAf+WnBqk/C402fBD9AVHxSIXMqjsY7MnYWKYEUqGixtr0c8+1DjzUEPlNgOYQPVrS1g== +rollup-plugin-terser@^5.1.2: + version "5.1.3" + resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-5.1.3.tgz#5f4c4603b12b4f8d093f4b6f31c9aa5eba98a223" + integrity sha512-FuFuXE5QUJ7snyxHLPp/0LFXJhdomKlIx/aK7Tg88Yubsx/UU/lmInoJafXJ4jwVVNcORJ1wRUC5T9cy5yk0wA== dependencies: "@babel/code-frame" "^7.0.0" jest-worker "^24.6.0" rollup-pluginutils "^2.8.1" - serialize-javascript "^1.7.0" + serialize-javascript "^2.1.2" terser "^4.1.0" -rollup-plugin-typescript2@^0.24.0: - version "0.24.3" - resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.24.3.tgz#276fa33a9d584d500da62d3e5400307f4a46bdf2" - integrity sha512-D7yovQlhnRoz7pG/RF0ni+koxgzEShwfAGuOq6OVqKzcATHOvmUt2ePeYVdc9N0adcW1PcTzklUEM0oNWE/POw== +rollup-plugin-typescript2@^0.25.2: + version "0.25.3" + resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.25.3.tgz#a5fb2f0f85488789334ce540abe6c7011cbdf40f" + integrity sha512-ADkSaidKBovJmf5VBnZBZe+WzaZwofuvYdzGAKTN/J4hN7QJCFYAq7IrH9caxlru6T5qhX41PNFS1S4HqhsGQg== dependencies: find-cache-dir "^3.0.0" fs-extra "8.1.0" @@ -6488,14 +6422,14 @@ rollup-pluginutils@2.8.1, rollup-pluginutils@^2.8.1: dependencies: estree-walker "^0.6.1" -rollup@^1.19.4: - version "1.21.4" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.21.4.tgz#00a41a30f90095db890301b226cbe2918e4cf54d" - integrity sha512-Pl512XVCmVzgcBz5h/3Li4oTaoDcmpuFZ+kdhS/wLreALz//WuDAMfomD3QEYl84NkDu6Z6wV9twlcREb4qQsw== +rollup@^1.27.14: + version "1.27.14" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.27.14.tgz#940718d5eec1a6887e399aa0089944bae5c4f377" + integrity sha512-DuDjEyn8Y79ALYXMt+nH/EI58L5pEw5HU9K38xXdRnxQhvzUTI/nxAawhkAHUQeudANQ//8iyrhVRHJBuR6DSQ== dependencies: - "@types/estree" "0.0.39" - "@types/node" "^12.7.5" - acorn "^7.0.0" + "@types/estree" "*" + "@types/node" "*" + acorn "^7.1.0" rsvp@^4.8.4: version "4.8.5" @@ -6513,22 +6447,22 @@ run-parallel@^1.1.9: integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== rxjs@^6.3.3: - version "6.5.2" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.2.tgz#2e35ce815cd46d84d02a209fb4e5921e051dbec7" - integrity sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg== + version "6.5.3" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a" + integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA== dependencies: tslib "^1.9.0" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@~5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + safe-json-stringify@~1: version "1.2.0" resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd" @@ -6581,20 +6515,15 @@ semver-compare@^1.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@^6.0.0, semver@^6.2.0: +semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@~6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.0.0.tgz#05e359ee571e5ad7ed641a6eec1e547ba52dea65" - integrity sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ== - -serialize-javascript@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.8.0.tgz#9515fc687232e2321aea1ca7a529476eb34bb480" - integrity sha512-3tHgtF4OzDmeKYj6V9nSyceRS0UJ3C7VqyD2Yj28vC/z2j6jG5FmFGahOKMD9CrglxTm3tETr87jEypaYV8DUg== +serialize-javascript@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" + integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" @@ -6644,11 +6573,23 @@ shebang-command@^1.2.0: dependencies: shebang-regex "^1.0.0" +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + shellwords@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" @@ -6679,10 +6620,10 @@ slice-ansi@0.0.4: resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU= -smart-buffer@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.0.2.tgz#5207858c3815cc69110703c6b94e46c15634395d" - integrity sha512-JDhEpTKzXusOqXZ0BUIdH+CjFdO/CR3tLlf5CN34IypI+xMmXW1uB16OOY8z3cICbJlDAVJzNbwBhNO0wt9OAw== +smart-buffer@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.1.0.tgz#91605c25d91652f4661ea69ccf45f1b331ca21ba" + integrity sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw== snapdragon-node@^2.0.1: version "2.1.1" @@ -6769,12 +6710,12 @@ socks-proxy-agent@^4.0.1: socks "~2.3.2" socks@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.3.2.tgz#ade388e9e6d87fdb11649c15746c578922a5883e" - integrity sha512-pCpjxQgOByDHLlNqlnh/mNSAxIUkyBBuwwhTcV+enZGbDaClPvHdvm6uvOwZfFJkam7cGhBNbb4JxiP8UZkRvQ== + version "2.3.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.3.3.tgz#01129f0a5d534d2b897712ed8aceab7ee65d78e3" + integrity sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA== dependencies: - ip "^1.1.5" - smart-buffer "4.0.2" + ip "1.1.5" + smart-buffer "^4.1.0" source-map-resolve@^0.5.0: version "0.5.2" @@ -6788,9 +6729,9 @@ source-map-resolve@^0.5.0: urix "^0.1.0" source-map-support@^0.5.6, source-map-support@~0.5.12: - version "0.5.13" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" - integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + version "0.5.16" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" + integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -6926,7 +6867,7 @@ stream-http@^3.0.0: readable-stream "^3.0.6" xtend "^4.0.0" -stream-meter@~1.0.4: +stream-meter@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/stream-meter/-/stream-meter-1.0.4.tgz#52af95aa5ea760a2491716704dbff90f73afdd1d" integrity sha1-Uq+Vql6nYKJJFxZwTb/5D3Ov3R0= @@ -6945,9 +6886,9 @@ streamroller@^1.0.6: lodash "^4.17.14" string-argv@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.0.tgz#0ea99e7257fea5e97a1bfcdfc19cf12d68e6ec6a" - integrity sha512-NGZHq3nkSXVtGZXTBjFru3MNfoZyIzN25T7BmvdgnSC0LCJczAGLLMQLyjywSIaAoqSemgLzBRHOsnrHbt60+Q== + version "0.3.1" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" + integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== string-length@^2.0.0: version "2.0.0" @@ -6983,6 +6924,31 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string.prototype.trimleft@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634" + integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +string.prototype.trimright@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58" + integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + string_decoder@^1.1.1, string_decoder@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -7025,13 +6991,20 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2, strip-ansi@^5.2.0: +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0, strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -7092,6 +7065,13 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + symbol-observable@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" @@ -7103,22 +7083,22 @@ symbol-tree@^3.2.2: integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== tar@^4: - version "4.4.10" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.10.tgz#946b2810b9a5e0b26140cf78bea6b0b0d689eba1" - integrity sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA== + version "4.4.13" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" + integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== dependencies: chownr "^1.1.1" fs-minipass "^1.2.5" - minipass "^2.3.5" + minipass "^2.8.6" minizlib "^1.2.1" mkdirp "^0.5.0" safe-buffer "^5.1.2" yallist "^3.0.3" terser@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.2.1.tgz#1052cfe17576c66e7bc70fcc7119f22b155bdac1" - integrity sha512-cGbc5utAcX4a9+2GGVX4DsenG6v0x3glnDi5hx8816X1McEAwPlPgRtXPJzSBsbpILxZ8MQMT0KvArLuE0HP5A== + version "4.3.10" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.3.10.tgz#094154a3ac30859b1e077c4be6c71b7cf4c09fac" + integrity sha512-3xXfyqHzUr8WZ+UvvwUQ/uHNgDu3FsdTTAL5p9UAWxlnAsoIMlCM3BPuFSx5Kb4/Hr+/xnMf6rt1DhRpKYohhw== dependencies: commander "^2.20.0" source-map "~0.6.1" @@ -7243,15 +7223,7 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -tough-cookie@^2.3.3, tough-cookie@^2.3.4: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - -tough-cookie@~2.4.3: +tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== @@ -7271,15 +7243,10 @@ trim-newlines@^1.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" - integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= - -ts-jest@^24.0.2: - version "24.1.0" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.1.0.tgz#2eaa813271a2987b7e6c3fefbda196301c131734" - integrity sha512-HEGfrIEAZKfu1pkaxB9au17b1d9b56YZSqz5eCVE8mX68+5reOvlM93xGOzzCREIov9mdH7JBG+s0UyNAqr0tQ== +ts-jest@^24.1.0: + version "24.2.0" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.2.0.tgz#7abca28c2b4b0a1fdd715cd667d65d047ea4e768" + integrity sha512-Yc+HLyldlIC9iIK8xEN7tV960Or56N49MDP7hubCZUeI7EbIOTsas6rXCMB4kQjLACJ7eDOF4xWEO5qumpKsag== dependencies: bs-logger "0.x" buffer-from "1.x" @@ -7292,10 +7259,10 @@ ts-jest@^24.0.2: semver "^5.5" yargs-parser "10.x" -ts-node@^8.1.0: - version "8.4.1" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.4.1.tgz#270b0dba16e8723c9fa4f9b4775d3810fd994b4f" - integrity sha512-5LpRN+mTiCs7lI5EtbXmF/HfMeCjzt7DH9CZwtkr6SywStrNQC723wG+aOWFiLNn7zT3kD/RnFqi3ZUfr4l5Qw== +ts-node@^8.4.1: + version "8.5.4" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.5.4.tgz#a152add11fa19c221d0b48962c210cf467262ab2" + integrity sha512-izbVCRV68EasEPQ8MSIGBNK9dc/4sYJJKYA+IarMQct1RtEot6Xp0bXuClsbUSnKpg50ho+aOAx8en5c+y4OFw== dependencies: arg "^4.1.0" diff "^4.0.1" @@ -7303,7 +7270,7 @@ ts-node@^8.1.0: source-map-support "^0.5.6" yn "^3.0.0" -tsconfig-paths@^3.8.0: +tsconfig-paths@^3.9.0: version "3.9.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== @@ -7318,22 +7285,22 @@ tslib@1.10.0, tslib@^1.10.0, tslib@^1.7.1, tslib@^1.8.0, tslib@^1.8.1, tslib@^1. resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== -tslint-config-prettier@1.15.x: - version "1.15.0" - resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.15.0.tgz#76b9714399004ab6831fdcf76d89b73691c812cf" - integrity sha512-06CgrHJxJmNYVgsmeMoa1KXzQRoOdvfkqnJth6XUkNeOz707qxN0WfxfhYwhL5kXHHbYJRby2bqAPKwThlZPhw== +tslint-config-prettier@^1.18.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz#75f140bde947d35d8f0d238e0ebf809d64592c37" + integrity sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg== -tslint-config-stoplight@~1.3: - version "1.3.0" - resolved "https://registry.yarnpkg.com/tslint-config-stoplight/-/tslint-config-stoplight-1.3.0.tgz#1e03815b5a6da19e001257e2530d66892cc1bd4f" - integrity sha512-X7W/+o5pI5dEytNqpMmpEgmY0MUfDBx6BB+XeLXmsqjOoHYt39XP9v44VneY2TkN9VmYCxgVaGqHjCP64qu4qw== +tslint-config-stoplight@~1.4: + version "1.4.0" + resolved "https://registry.yarnpkg.com/tslint-config-stoplight/-/tslint-config-stoplight-1.4.0.tgz#07eea5e02395fd3d58c67153334501efabcbe11c" + integrity sha512-C1tfJAO5ybMEj62u2HEpsGPvSKWnNxcOtAuiLlwTb7vf++jnyGYeqEW8oE69jefjwo/s8VxBy9Sw7IAAh5ypsQ== dependencies: - prettier "1.14.x" - tslint "5.11.x" - tslint-config-prettier "1.15.x" - tslint-plugin-prettier "2.0.x" + prettier "^1.19.1" + tslint "^5.20.1" + tslint-config-prettier "^1.18.0" + tslint-plugin-prettier "^2.0.1" -tslint-plugin-prettier@2.0.x: +tslint-plugin-prettier@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/tslint-plugin-prettier/-/tslint-plugin-prettier-2.0.1.tgz#95b6a3b766622ffc44375825d7760225c50c3680" integrity sha512-4FX9JIx/1rKHIPJNfMb+ooX1gPk5Vg3vNi7+dyFYpLO+O57F4g+b/fo1+W/G0SUOkBLHB/YKScxjX/P+7ZT/Tw== @@ -7342,28 +7309,10 @@ tslint-plugin-prettier@2.0.x: lines-and-columns "^1.1.6" tslib "^1.7.1" -tslint@5.11.x: - version "5.11.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.11.0.tgz#98f30c02eae3cde7006201e4c33cb08b48581eed" - integrity sha1-mPMMAurjzecAYgHkwzywi0hYHu0= - dependencies: - babel-code-frame "^6.22.0" - builtin-modules "^1.1.1" - chalk "^2.3.0" - commander "^2.12.1" - diff "^3.2.0" - glob "^7.1.1" - js-yaml "^3.7.0" - minimatch "^3.0.4" - resolve "^1.3.2" - semver "^5.3.0" - tslib "^1.8.0" - tsutils "^2.27.2" - -tslint@~5.20: - version "5.20.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.0.tgz#fac93bfa79568a5a24e7be9cdde5e02b02d00ec1" - integrity sha512-2vqIvkMHbnx8acMogAERQ/IuINOq6DFqgF8/VDvhEkBqQh/x6SP0Y+OHnKth9/ZcHQSroOZwUQSN18v8KKF0/g== +tslint@^5.20.1, tslint@~5.20: + version "5.20.1" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.1.tgz#e401e8aeda0152bc44dd07e614034f3f80c67b7d" + integrity sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg== dependencies: "@babel/code-frame" "^7.0.0" builtin-modules "^1.1.1" @@ -7379,7 +7328,7 @@ tslint@~5.20: tslib "^1.8.0" tsutils "^2.29.0" -tsutils@^2.27.2, tsutils@^2.29.0: +tsutils@^2.29.0: version "2.29.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== @@ -7428,10 +7377,10 @@ type-is@~1.6.17: media-typer "0.3.0" mime-types "~2.1.24" -typescript-json-schema@~0.40: - version "0.40.0" - resolved "https://registry.yarnpkg.com/typescript-json-schema/-/typescript-json-schema-0.40.0.tgz#4815092e5acf1662a94aa140924809ff5922da7c" - integrity sha512-C8D3Ca6+1x3caWOR+u45Shn3KqkRZi5M3+E8ePpEmYMqOh3xhhLdq+39pqT0Bf8+fCgAmpTFSJMT6Xwqbm0Tkw== +typescript-json-schema@~0.41.0: + version "0.41.0" + resolved "https://registry.yarnpkg.com/typescript-json-schema/-/typescript-json-schema-0.41.0.tgz#8dbe1798023ae50e803b3f9d53a6d10aaf5a9d74" + integrity sha512-ThW7KtuMUR+kZ9Id0G+hJ0YLp3DWPjL6NkrOqAyaKttO3839wF4jiYKm7mZcpUe1BcHs12pfbEo40+qYQtm4yA== dependencies: "@types/json-schema" "^7.0.3" glob "~7.1.4" @@ -7439,17 +7388,17 @@ typescript-json-schema@~0.40: typescript "^3.5.3" yargs "^14.0.0" -typescript@^3.5.3: - version "3.6.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.3.tgz#fea942fabb20f7e1ca7164ff626f1a9f3f70b4da" - integrity sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw== +typescript@^3.5.3, typescript@^3.7.4: + version "3.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.4.tgz#1743a5ec5fef6a1fa9f3e4708e33c81c73876c19" + integrity sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw== uglify-js@^3.1.4: - version "3.6.0" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5" - integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg== + version "3.6.8" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.8.tgz#5edcbcf9d49cbb0403dc49f856fe81530d65145e" + integrity sha512-XhHJ3S3ZyMwP8kY1Gkugqx3CJh2C3O0y8NPiSxtm1tyD/pktLAkFZsFGpuNfTZddKDQ/bbDBLAd2YyA1pbi8HQ== dependencies: - commander "~2.20.0" + commander "~2.20.3" source-map "~0.6.1" uid2@0.0.3: @@ -7477,7 +7426,7 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^2.0.1" -unique-temp-dir@~1.0.0: +unique-temp-dir@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unique-temp-dir/-/unique-temp-dir-1.0.0.tgz#6dce95b2681ca003eebfb304a415f9cbabcc5385" integrity sha1-bc6VsmgcoAPuv7MEpBX5y6vMU4U= @@ -7512,9 +7461,9 @@ uri-js@^4.2.2: punycode "^2.1.0" urijs@~1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.1.tgz#5b0ff530c0cbde8386f6342235ba5ca6e995d25a" - integrity sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg== + version "1.19.2" + resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.2.tgz#f9be09f00c4c5134b7cb3cf475c1dd394526265a" + integrity sha512-s/UIq9ap4JPZ7H1EB5ULo/aOUbWqfDi7FKzMC2Nz+0Si8GiT1rIEaprt8hy3Vy2Ex2aJPpOQv4P4DuOZ+K1c6w== urix@^0.1.0: version "0.1.0" @@ -7547,7 +7496,7 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -util.promisify@^1.0.0, util.promisify@~1.0.0: +util.promisify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== @@ -7603,11 +7552,6 @@ void-elements@^2.0.0: resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= -vscode-uri@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-2.0.3.tgz#25e5f37f552fbee3cec7e5f80cef8469cefc6543" - integrity sha512-4D3DI3F4uRy09WNtDGD93H9q034OHImxiIcSq664Hq1Y1AScehlP3qqZyTkX/RWxeu0MRMHGkrxYqm2qlDF/aw== - w3c-hr-time@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045" @@ -7656,9 +7600,9 @@ whatwg-url@^6.4.1, whatwg-url@^6.5.0: webidl-conversions "^4.0.2" whatwg-url@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd" - integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ== + version "7.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== dependencies: lodash.sortby "^4.7.0" tr46 "^1.0.1" @@ -7676,6 +7620,13 @@ which@^1.1.1, which@^1.2.1, which@^1.2.12, which@^1.2.9, which@^1.3.0: dependencies: isexe "^2.0.0" +which@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.1.tgz#f1cf94d07a8e571b6ff006aeb91d0300c47ef0a4" + integrity sha512-N7GBZOTswtB9lkQBZA4+zAXrjEIWAUOB93AvzUiudRzRxhUdLURQ7D/gAIMY1gatT/LTbmbcv8SiYazy3eYB7w== + dependencies: + isexe "^2.0.0" + wide-align@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" @@ -7683,7 +7634,12 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2 || 2" -wordwrap@^1.0.0, wordwrap@~1.0.0: +word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wordwrap@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= @@ -7710,6 +7666,15 @@ wrap-ansi@^5.1.0: string-width "^3.0.0" strip-ansi "^5.0.0" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -7745,13 +7710,12 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== -xml2js@^0.4.21: - version "0.4.22" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.22.tgz#4fa2d846ec803237de86f30aa9b5f70b6600de02" - integrity sha512-MWTbxAQqclRSTnehWWe5nMKzI3VmJ8ltiJEco8akcC6j3miOhjjfzKum5sId+CWhfxdOs/1xauYr8/ZDBtQiRw== +xml2js@^0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== dependencies: sax ">=0.6.0" - util.promisify "~1.0.0" xmlbuilder "~11.0.0" xmlbuilder@~11.0.0: @@ -7784,10 +7748,10 @@ yallist@^2.1.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= -yallist@^3.0.0, yallist@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" - integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== +yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== yargs-parser@10.x: version "10.1.0" @@ -7804,6 +7768,22 @@ yargs-parser@^13.1.1: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^15.0.0: + version "15.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.0.tgz#cdd7a97490ec836195f59f3f4dbe5ea9e8f75f08" + integrity sha512-xLTUnCMc4JhxrPEPUYD5IBR1mWCK/aT6+RJ/K29JY2y1vD+FhtgKK0AXRWvI262q3QSffAQuTouFIKUuHX89wQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^16.1.0: + version "16.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-16.1.0.tgz#73747d53ae187e7b8dbe333f95714c76ea00ecf1" + integrity sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs@^13.2.4, yargs@^13.3.0: version "13.3.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" @@ -7821,9 +7801,9 @@ yargs@^13.2.4, yargs@^13.3.0: yargs-parser "^13.1.1" yargs@^14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.0.0.tgz#ba4cacc802b3c0b3e36a9e791723763d57a85066" - integrity sha512-ssa5JuRjMeZEUjg7bEL99AwpitxU/zWGAGpdj0di41pOEmJti8NR6kyUIJBkR78DTYNPZOU08luUo0GTHuB+ow== + version "14.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.0.tgz#f116a9242c4ed8668790b40759b4906c276e76c3" + integrity sha512-/is78VKbKs70bVZH7w4YaZea6xcJWOAwkhbR0CFuZBmYtfTYF0xjGJF43AYd8g2Uii1yJwmS5GR2vBmrc32sbg== dependencies: cliui "^5.0.0" decamelize "^1.2.0" @@ -7835,7 +7815,24 @@ yargs@^14.0.0: string-width "^3.0.0" which-module "^2.0.0" y18n "^4.0.0" - yargs-parser "^13.1.1" + yargs-parser "^15.0.0" + +yargs@^15.0.2: + version "15.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.0.2.tgz#4248bf218ef050385c4f7e14ebdf425653d13bd3" + integrity sha512-GH/X/hYt+x5hOat4LMnCqMd8r5Cv78heOMIJn1hr7QPPBqfeC6p89Y78+WB9yGDvfpCvgasfmWLzNzEioOUD9Q== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^16.1.0" yeast@0.1.2: version "0.1.2"