Repository: Redocly/redocly-cli
Pull request #1055 (2023-04-19 14:26:29)

fix: show error if apis or rules not provided
## What/Why/How?

 - show error if apis for lint not provided
 -  show error if  rules or extends not defined in the existing config file 

## Reference
Closes: https://github.com/Redocly/redocly-cli/issues/1054
## Testing

## Screenshots (optional)

## Check yourself

- [x] Code is linted
- [ ] Tested with redoc/reference-docs/workflows
- [x] All new/updated code is covered with tests

## Security

- [x] Security impact of change has been considered
- [x] Code follows company security practices and guidelines


github-actions[bot]: <!-- jest coverage report action for options with hash 3339ae53a38c44382c0fb4491bdb282d -->

## Coverage report 



| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | Category   | Percentage | Covered / Total |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :--------- | :--------- | :-------------: |
|                                                                           🟡                                                                           | Statements | 73.81%     |    3464/4693    |
|                                                                           🟡                                                                           | Branches   | 64.78%     |    1970/3041    |
|                                                                           🟡                                                                           | Functions  | 66.02%     |     581/880     |
|                                                                           🟡                                                                           | Lines      | 73.83%     |    3222/4364    |


## Test suite run success
577 tests passing in 85 suites.

<p align="right">Report generated by <a href="https://github.com/ArtiomTr/jest-coverage-report-action">🧪jest coverage report action</a> from a1f8bddea72c952cc22dd13472761fe0c05ed7bf</p>


Patch:
diff --git a/packages/cli/src/__mocks__/utils.ts b/packages/cli/src/__mocks__/utils.ts
index 2c2dc06c..bebdc01d 100644
--- a/packages/cli/src/__mocks__/utils.ts
+++ b/packages/cli/src/__mocks__/utils.ts
@@ -15,4 +15,5 @@ export const handleError = jest.fn();
 export const exitWithError = jest.fn();
 export const writeYaml = jest.fn();
 export const loadConfigAndHandleErrors = jest.fn(() => ConfigFixture);
+export const checkIfRulesetExist = jest.fn();
 export const sortTopLevelKeysForOas = jest.fn((document) => document);
diff --git a/packages/cli/src/commands/bundle.ts b/packages/cli/src/commands/bundle.ts
index 246be44b..95cb0dac 100644
--- a/packages/cli/src/commands/bundle.ts
+++ b/packages/cli/src/commands/bundle.ts
@@ -9,6 +9,7 @@ import {
   saveBundle,
   printLintTotals,
   loadConfigAndHandleErrors,
+  checkIfRulesetExist,
   sortTopLevelKeysForOas,
 } from '../utils';
 import type { CommonOptions, OutputExtensions, Skips, Totals } from '../types';
@@ -51,6 +52,7 @@ export async function handleBundle(argv: BundleOptions, version: string) {
       styleguide.skipDecorators(argv['skip-decorator']);
 
       if (argv.lint) {
+        checkIfRulesetExist(styleguide.rules);
         if (config.styleguide.recommendedFallback) {
           process.stderr.write(
             `No configurations were provided -- using built in ${blue(
diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts
index 1b615019..a724ebc2 100644
--- a/packages/cli/src/commands/lint.ts
+++ b/packages/cli/src/commands/lint.ts
@@ -1,28 +1,29 @@
 import {
   Config,
+  doesYamlFileExist,
+  findConfig,
   formatProblems,
+  getMergedConfig,
   getTotals,
   lint,
   lintConfig,
-  findConfig,
-  getMergedConfig,
   makeDocumentFromString,
-  stringifyYaml,
+  ProblemSeverity,
   RawConfig,
   RuleSeverity,
-  ProblemSeverity,
-  doesYamlFileExist,
+  stringifyYaml,
 } from '@redocly/openapi-core';
 import {
+  checkIfRulesetExist,
+  exitWithError,
   getExecutionTime,
   getFallbackApisOrExit,
   handleError,
+  loadConfigAndHandleErrors,
   pluralize,
-  printLintTotals,
   printConfigLintTotals,
+  printLintTotals,
   printUnusedWarnings,
-  exitWithError,
-  loadConfigAndHandleErrors,
 } from '../utils';
 import type { CommonOptions, Skips, Totals } from '../types';
 import { blue, gray } from 'colorette';
@@ -47,6 +48,10 @@ export async function handleLint(argv: LintOptions, version: string) {
 
   const apis = await getFallbackApisOrExit(argv.apis, config);
 
+  if (!apis.length) {
+    return exitWithError('No APIs were provided');
+  }
+
   if (argv['generate-ignore-file']) {
     config.styleguide.ignore = {}; // clear ignore
   }
@@ -60,6 +65,8 @@ export async function handleLint(argv: LintOptions, version: string) {
       const resolvedConfig = getMergedConfig(config, alias);
       const { styleguide } = resolvedConfig;
 
+      checkIfRulesetExist(styleguide.rules);
+
       styleguide.skipRules(argv['skip-rule']);
       styleguide.skipPreprocessors(argv['skip-preprocessor']);
 
diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts
index 1106b29c..551e00fc 100644
--- a/packages/cli/src/utils.ts
+++ b/packages/cli/src/utils.ts
@@ -22,6 +22,7 @@ import {
   Oas2Definition,
 } from '@redocly/openapi-core';
 import { Totals, outputExtensions, Entrypoint, ConfigApis } from './types';
+import { isEmptyObject } from '@redocly/openapi-core/lib/utils';
 
 export async function getFallbackApisOrExit(
   argsApis: string[] | undefined,
@@ -455,3 +456,17 @@ function sortOas3Keys(document: Oas3Definition): Oas3Definition {
   // merge any other top-level keys (e.g. vendor extensions)
   return Object.assign(result, document);
 }
+
+export function checkIfRulesetExist(rules: typeof StyleguideConfig.prototype.rules) {
+  const ruleset = {
+    ...rules.oas2,
+    ...rules.oas3_0,
+    ...rules.oas3_0,
+  };
+
+  if (isEmptyObject(ruleset)) {
+    exitWithError(
+      '⚠️ No rules were configured. Learn how to configure rules: https://redocly.com/docs/cli/rules/'
+    );
+  }
+}


Test patch:
diff --git a/__tests__/bundle/bundle-anchor/snapshot.js b/__tests__/bundle/bundle-anchor/snapshot.js
index 3443ded3..756fb6ef 100644
--- a/__tests__/bundle/bundle-anchor/snapshot.js
+++ b/__tests__/bundle/bundle-anchor/snapshot.js
@@ -28,8 +28,6 @@ paths:
                 foo: TEST
 components: {}
 
-Woohoo! Your OpenAPI definitions are valid. 🎉
-
 bundling ./test.yaml...
 📦 Created a bundle for ./test.yaml at stdout <test>ms.
 
diff --git a/__tests__/bundle/bundle-description-dereferenced/snapshot.js b/__tests__/bundle/bundle-description-dereferenced/snapshot.js
index 64e2cf9f..df842b8c 100644
--- a/__tests__/bundle/bundle-description-dereferenced/snapshot.js
+++ b/__tests__/bundle/bundle-description-dereferenced/snapshot.js
@@ -36,13 +36,6 @@ components:
           $ref: '#/components/schemas/Name'
           description: Other name (specific).
 
-test.yaml:
-  1:1  error    spec  The field \`info\` must be present on this level.
-
-< ... 2 more problems hidden > increase with \`--max-problems N\`
-❌ Validation failed with 2 errors and 1 warning.
-run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file.
-
 bundling ./test.yaml...
 📦 Created a bundle for ./test.yaml at stdout <test>ms.
 
diff --git a/__tests__/bundle/bundle-description-long/snapshot.js b/__tests__/bundle/bundle-description-long/snapshot.js
index 5ec3632e..6c604957 100644
--- a/__tests__/bundle/bundle-description-long/snapshot.js
+++ b/__tests__/bundle/bundle-description-long/snapshot.js
@@ -18,12 +18,6 @@ info:
 security: []
 components: {}
 
-test.yaml:
-  1:1  error    no-empty-servers  Servers must be present.
-
-❌ Validation failed with 1 error.
-run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file.
-
 bundling ./test.yaml...
 📦 Created a bundle for ./test.yaml at stdout <test>ms.
 
diff --git a/__tests__/bundle/info-description-override-error/snapshot.js b/__tests__/bundle/info-description-override-error/snapshot.js
index e5263405..8d5e3e9d 100644
--- a/__tests__/bundle/info-description-override-error/snapshot.js
+++ b/__tests__/bundle/info-description-override-error/snapshot.js
@@ -2,8 +2,6 @@
 
 exports[`E2E bundle info-description-override-error 1`] = `
 
-Woohoo! Your OpenAPI definitions are valid. 🎉
-
 bundling ./main.yaml...
 main.yaml:
   8:16  error    info-description-override  Failed to read markdown override file for "info.description".
diff --git a/__tests__/bundle/info-description-override/snapshot.js b/__tests__/bundle/info-description-override/snapshot.js
index 3dc3924a..b9976700 100644
--- a/__tests__/bundle/info-description-override/snapshot.js
+++ b/__tests__/bundle/info-description-override/snapshot.js
@@ -31,8 +31,6 @@ paths:
           description: example description
 components: {}
 
-Woohoo! Your OpenAPI definitions are valid. 🎉
-
 bundling ./main.yaml...
 📦 Created a bundle for ./main.yaml at stdout <test>ms.
 
diff --git a/__tests__/bundle/info-override/snapshot.js b/__tests__/bundle/info-override/snapshot.js
index 7683e80e..91fb8123 100644
--- a/__tests__/bundle/info-override/snapshot.js
+++ b/__tests__/bundle/info-override/snapshot.js
@@ -27,8 +27,6 @@ paths:
           description: example description
 components: {}
 
-Woohoo! Your OpenAPI definitions are valid. 🎉
-
 bundling ./main.yaml...
 📦 Created a bundle for ./main.yaml at stdout <test>ms.
 
diff --git a/__tests__/bundle/operation-description-override-error/snapshot.js b/__tests__/bundle/operation-description-override-error/snapshot.js
index af77bc14..9a52cc03 100644
--- a/__tests__/bundle/operation-description-override-error/snapshot.js
+++ b/__tests__/bundle/operation-description-override-error/snapshot.js
@@ -2,8 +2,6 @@
 
 exports[`E2E bundle operation-description-override-error 1`] = `
 
-Woohoo! Your OpenAPI definitions are valid. 🎉
-
 bundling ./openapi.yaml...
 openapi.yaml:
   23:7  error    operation-description-override  Failed to read markdown override file for operation "updatePet".
diff --git a/__tests__/bundle/operation-description-override/snapshot.js b/__tests__/bundle/operation-description-override/snapshot.js
index d2ccdc9a..56628ed3 100644
--- a/__tests__/bundle/operation-description-override/snapshot.js
+++ b/__tests__/bundle/operation-description-override/snapshot.js
@@ -47,8 +47,6 @@ paths:
           description: ok
 components: {}
 
-Woohoo! Your OpenAPI definitions are valid. 🎉
-
 bundling ./openapi.yaml...
 📦 Created a bundle for ./openapi.yaml at stdout <test>ms.
 
diff --git a/__tests__/bundle/primitive-types/snapshot.js b/__tests__/bundle/primitive-types/snapshot.js
index 41cb25a8..0f752896 100644
--- a/__tests__/bundle/primitive-types/snapshot.js
+++ b/__tests__/bundle/primitive-types/snapshot.js
@@ -40,8 +40,6 @@ paths:
                     key2: 2
 components: {}
 
-Woohoo! Your OpenAPI definitions are valid. 🎉
-
 bundling ./openapi.yaml...
 📦 Created a bundle for ./openapi.yaml at stdout <test>ms.
 
diff --git a/__tests__/bundle/resolve-refs-from-x-attributes/.redocly.yaml b/__tests__/bundle/resolve-refs-from-x-attributes/.redocly.yaml
index 11af78b5..d436b7ad 100644
--- a/__tests__/bundle/resolve-refs-from-x-attributes/.redocly.yaml
+++ b/__tests__/bundle/resolve-refs-from-x-attributes/.redocly.yaml
@@ -1,6 +1,7 @@
 apis:
   main:
     root: ./openapi.yaml
+
 decorators:
   plugin/resolve-x: on
 plugins:
diff --git a/__tests__/bundle/resolve-refs-from-x-attributes/openapi.yaml b/__tests__/bundle/resolve-refs-from-x-attributes/openapi.yaml
index 3c9f21db..27a6b0ca 100644
--- a/__tests__/bundle/resolve-refs-from-x-attributes/openapi.yaml
+++ b/__tests__/bundle/resolve-refs-from-x-attributes/openapi.yaml
@@ -2,6 +2,9 @@ openapi: 3.0.3
 info:
   title: foo
   version: 1.0
+  license:
+    name: Apache 2.0
+    url: https://www.apache.org/licenses/LICENSE-2.0.html
   x-attributes:
     - $ref: external.yaml#/Test1
   test-attributes:
diff --git a/__tests__/bundle/resolve-refs-from-x-attributes/snapshot.js b/__tests__/bundle/resolve-refs-from-x-attributes/snapshot.js
index 0c1732f0..0e21318e 100644
--- a/__tests__/bundle/resolve-refs-from-x-attributes/snapshot.js
+++ b/__tests__/bundle/resolve-refs-from-x-attributes/snapshot.js
@@ -5,6 +5,9 @@ openapi: 3.0.3
 info:
   title: foo
   version: 1
+  license:
+    name: Apache 2.0
+    url: https://www.apache.org/licenses/LICENSE-2.0.html
   x-attributes:
     - $ref: external.yaml#/Test1
       resolved:
@@ -15,8 +18,6 @@ info:
         name: test 2
 components: {}
 
-Woohoo! Your OpenAPI definitions are valid. 🎉
-
 bundling ./openapi.yaml...
 📦 Created a bundle for ./openapi.yaml at stdout <test>ms.
 
diff --git a/__tests__/bundle/tag-description-override-error/snapshot.js b/__tests__/bundle/tag-description-override-error/snapshot.js
index 7d0f86ad..8d197842 100644
--- a/__tests__/bundle/tag-description-override-error/snapshot.js
+++ b/__tests__/bundle/tag-description-override-error/snapshot.js
@@ -2,8 +2,6 @@
 
 exports[`E2E bundle tag-description-override-error 1`] = `
 
-Woohoo! Your OpenAPI definitions are valid. 🎉
-
 bundling ./main.yaml...
 main.yaml:
   17:5  error    tag-description-override  Failed to read markdown override file for tag "pet".
diff --git a/__tests__/bundle/tag-description-override/snapshot.js b/__tests__/bundle/tag-description-override/snapshot.js
index 239177e8..df3a1994 100644
--- a/__tests__/bundle/tag-description-override/snapshot.js
+++ b/__tests__/bundle/tag-description-override/snapshot.js
@@ -43,8 +43,6 @@ paths:
           description: example description
 components: {}
 
-Woohoo! Your OpenAPI definitions are valid. 🎉
-
 bundling ./main.yaml...
 📦 Created a bundle for ./main.yaml at stdout <test>ms.
 
diff --git a/__tests__/commands.test.ts b/__tests__/commands.test.ts
index a1bf77b6..4d0f0ef3 100644
--- a/__tests__/commands.test.ts
+++ b/__tests__/commands.test.ts
@@ -205,6 +205,7 @@ describe('E2E', () => {
       'bundle-remove-unused-components',
       'bundle-remove-unused-components-from-config',
       'bundle-lint-format',
+      'max-problems-argument',
     ];
     const folderPath = join(__dirname, 'bundle');
     const contents = readdirSync(folderPath).filter((folder) => !excludeFolders.includes(folder));
@@ -218,8 +219,6 @@ describe('E2E', () => {
       const entryPoints = getEntrypoints(testPath);
 
       const args = getParams('../../../packages/cli/src/index.ts', 'bundle', [
-        '--lint',
-        '--max-problems=1',
         '--format=stylish',
         ...entryPoints,
       ]);
@@ -229,6 +228,19 @@ describe('E2E', () => {
         (<any>expect(result)).toMatchSpecificSnapshot(join(testPath, 'snapshot.js'));
       });
     }
+
+    it('max-problems-argument', () => {
+      const folderPath = join(__dirname, 'bundle/max-problems-argument');
+      const entryPoints = getEntrypoints(folderPath);
+      const args = getParams('../../../packages/cli/src/index.ts', 'bundle', [
+        '--lint',
+        '--max-problems=1',
+        '--format=stylish',
+        ...entryPoints,
+      ]);
+      const result = getCommandOutput(args, folderPath);
+      (<any>expect(result)).toMatchSpecificSnapshot(join(folderPath, 'snapshot.js'));
+    });
   });
 
   describe('bundle lint format', () => {
diff --git a/__tests__/lint-config/invalid-config-format-json/redocly.yaml b/__tests__/lint-config/invalid-config-format-json/redocly.yaml
index 91bc26f5..3e4a944a 100644
--- a/__tests__/lint-config/invalid-config-format-json/redocly.yaml
+++ b/__tests__/lint-config/invalid-config-format-json/redocly.yaml
@@ -1,1 +1,4 @@
-badKey: 1
\ No newline at end of file
+badKey: 1
+
+extends:
+  - recommended
diff --git a/__tests__/lint/extends-empty-array/.redocly.yaml b/__tests__/lint/extends-empty-array/.redocly.yaml
new file mode 100644
index 00000000..0992733a
--- /dev/null
+++ b/__tests__/lint/extends-empty-array/.redocly.yaml
@@ -0,0 +1,5 @@
+apis:
+  main:
+    root: openapi.yaml
+
+extends: []
diff --git a/__tests__/lint/extends-empty-array/openapi.yaml b/__tests__/lint/extends-empty-array/openapi.yaml
new file mode 100644
index 00000000..68cb0ad2
--- /dev/null
+++ b/__tests__/lint/extends-empty-array/openapi.yaml
@@ -0,0 +1,18 @@
+openapi: 3.1.0
+info:
+  title: Example OpenAPI 3 definition.
+  version: 1.0.0
+
+paths:
+  '/ping/{id}/{test}':
+    get:
+      parameters:
+        - in: path
+          name: test_id
+          description: User id
+          required: true
+          schema:
+            type: string
+      responses:
+        '200':
+          description: example description
diff --git a/__tests__/lint/extends-empty-array/snapshot.js b/__tests__/lint/extends-empty-array/snapshot.js
new file mode 100644
index 00000000..dd720701
--- /dev/null
+++ b/__tests__/lint/extends-empty-array/snapshot.js
@@ -0,0 +1,8 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`E2E lint extends-empty-array 1`] = `
+
+⚠️ No rules were configured. Learn how to configure rules: https://redocly.com/docs/cli/rules/
+
+
+`;
diff --git a/__tests__/lint/turn-off-rules/snapshot.js b/__tests__/lint/turn-off-rules/snapshot.js
index a5245f15..e449c5d5 100644
--- a/__tests__/lint/turn-off-rules/snapshot.js
+++ b/__tests__/lint/turn-off-rules/snapshot.js
@@ -2,10 +2,7 @@
 
 exports[`E2E lint turn-off-rules 1`] = `
 
-validating /openapi.yaml...
-/openapi.yaml: validated in <test>ms
-
-Woohoo! Your OpenAPI definition is valid. 🎉
+⚠️ No rules were configured. Learn how to configure rules: https://redocly.com/docs/cli/rules/
 
 
 `;
diff --git a/__tests__/zero-config/no-default-recommended-fallback/snapshot.js b/__tests__/zero-config/no-default-recommended-fallback/snapshot.js
index 75391bd7..ffdb4e61 100644
--- a/__tests__/zero-config/no-default-recommended-fallback/snapshot.js
+++ b/__tests__/zero-config/no-default-recommended-fallback/snapshot.js
@@ -2,10 +2,7 @@
 
 exports[`E2E zero-config no-default-recommended-fallback 1`] = `
 
-validating /openapi.yaml...
-/openapi.yaml: validated in <test>ms
-
-Woohoo! Your OpenAPI definition is valid. 🎉
+⚠️ No rules were configured. Learn how to configure rules: https://redocly.com/docs/cli/rules/
 
 
 `;
diff --git a/packages/cli/src/__tests__/commands/lint.test.ts b/packages/cli/src/__tests__/commands/lint.test.ts
index 13bfbf9a..1149935d 100644
--- a/packages/cli/src/__tests__/commands/lint.test.ts
+++ b/packages/cli/src/__tests__/commands/lint.test.ts
@@ -13,6 +13,7 @@ import {
   handleError,
   exitWithError,
   loadConfigAndHandleErrors,
+  checkIfRulesetExist,
 } from '../../utils';
 import { ConfigFixture } from '../fixtures/config';
 import { performance } from 'perf_hooks';
@@ -87,6 +88,17 @@ describe('handleLint', () => {
       await handleLint({ ...argvMock, 'generate-ignore-file': true }, versionMock);
       expect(getMergedConfigMock).toHaveBeenCalled();
     });
+
+    it('should check if ruleset exist', async () => {
+      await handleLint(argvMock, versionMock);
+      expect(checkIfRulesetExist).toHaveBeenCalledTimes(1);
+    });
+
+    it('should fail if apis not provided', async () => {
+      await handleLint({ ...argvMock, apis: [] }, versionMock);
+      expect(getFallbackApisOrExit).toHaveBeenCalledTimes(1);
+      expect(exitWithError).toHaveBeenCalledWith('No APIs were provided');
+    });
   });
 
   describe('loop through entrypints and lint stage', () => {
diff --git a/packages/cli/src/__tests__/utils.test.ts b/packages/cli/src/__tests__/utils.test.ts
index b5a424e4..6172bdb1 100644
--- a/packages/cli/src/__tests__/utils.test.ts
+++ b/packages/cli/src/__tests__/utils.test.ts
@@ -4,6 +4,7 @@ import {
   pathToFilename,
   printConfigLintTotals,
   langToExt,
+  checkIfRulesetExist,
   handleError,
   CircularJSONNotSupportedError,
   sortTopLevelKeysForOas,
@@ -408,3 +409,33 @@ describe('handleErrors', () => {
     );
   });
 });
+
+describe('checkIfRulesetExist', () => {
+  beforeEach(() => {
+    jest.spyOn(process, 'exit').mockImplementation((code?: number) => code as never);
+  });
+
+  afterEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('should exit if rules not provided', () => {
+    const rules = {
+      oas2: {},
+      oas3_0: {},
+      oas3_1: {},
+    };
+    checkIfRulesetExist(rules);
+    expect(process.exit).toHaveBeenCalledWith(1);
+  });
+
+  it('should not exit if rules provided', () => {
+    const rules = {
+      oas2: { 'operation-4xx-response': 'error' },
+      oas3_0: {},
+      oas3_1: {},
+    } as any;
+    checkIfRulesetExist(rules);
+    expect(process.exit).not.toHaveBeenCalled();
+  });
+});