Repository: stoplightio/spectral (ts)

Issue:
Wrongly declared array not caught as error
**Describe the bug**
The OAS file below has a wrongly declared array in the response. It states `type: array` and last line, and is missing an `items` key. Spectral does not catch this as an error. However, deploying such OAS in Apigee will not work as Apigeee uses Swagger parser which catches this error.

```yaml
openapi: 3.0.3
info:
  title: Wrong array
  description: Wrong array
  version: 1.0.0
  contact:
    name: IDA Support Team
    email: ida_team@maersk.com
    url: https://www.maersk.com/contactus
  license:
    url: https://terms.maersk.com/api-license-terms
    name: Maersk 1.0
servers:
  - url: https://api-stage.net
paths:
  /translatetoen:
    post:
      operationId: post-translation-request
      description: Sent a text for translation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                inputText:
                  type: string
              required:
                - inputText
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  highestPrecendenceLanguage:
                    type: string
                  translatedText:
                    type: string
                  allIdentifiedLanguages:
                    type: array
                    items:
                      type: array
```

**To Reproduce**

1. Use above OAS, create file `oas-array-wrong.yaml`
2. Run CLI `spectral lint oas-array-wrong.yaml`
3. Output is `No results with a severity of 'error' found!`

**Expected behavior**
The wrongly declared array should be treated as an error as you cannot have an array declared without stating what type the items are.

**Screenshots**
NA

**Environment (remove any that are not applicable):**
 - Library version: 6.11.0
 - OS: MacOS
 - Browser: NA


Pull request description:
feat(rulesets): add new rule that requires sibling items field for type array
Fixes #2595.

This PR adds a new `array-items` rule for OAS 3.0.x which requires schemas with "type: array" to have a sibling "items" field.

There are some additional changes present that make tests more stable. 

**Checklist**

- [x] Tests added / updated
- [ ] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No

Patch:
diff --git a/.nvmrc b/.nvmrc
index b009dfb9..a58d2d2c 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1,1 +1,1 @@
-lts/*
+18.18.2
\ No newline at end of file
diff --git a/docs/reference/openapi-rules.md b/docs/reference/openapi-rules.md
index e3884880..7b2e5118 100644
--- a/docs/reference/openapi-rules.md
+++ b/docs/reference/openapi-rules.md
@@ -448,6 +448,37 @@ TheBadModel:
         - 8
 ```
 
+### array-items
+
+Schemas with `type: array`, require a sibling `items` field.
+
+**Recommended:** Yes
+
+**Good Example**
+
+```yaml
+TheGoodModel:
+  type: object
+  properties:
+    favoriteColorSets:
+      type: array
+      items:
+        type: array
+        items: {}
+```
+
+**Bad Example**
+
+```yaml
+TheBadModel:
+  type: object
+  properties:
+    favoriteColorSets:
+      type: array
+      items:
+        type: array
+```
+
 ## OpenAPI v2.0-only
 
 These rules will only apply to OpenAPI v2.0 documents.
diff --git a/packages/rulesets/src/oas/index.ts b/packages/rulesets/src/oas/index.ts
index 25367375..d59122c2 100644
--- a/packages/rulesets/src/oas/index.ts
+++ b/packages/rulesets/src/oas/index.ts
@@ -361,6 +361,18 @@ const ruleset = {
         function: refSiblings,
       },
     },
+    'array-items': {
+      formats: [oas3_0],
+      message: 'Schemas with "type: array", require a sibling "items" field',
+      severity: 0,
+      recommended: true,
+      resolved: false,
+      given: "$..[?(@.type === 'array')]",
+      then: {
+        function: truthy,
+        field: 'items',
+      },
+    },
     'typed-enum': {
       description: 'Enum values must respect the specified type.',
       message: '{{error}}',


Test patch:
diff --git a/packages/rulesets/src/oas/__tests__/array-items.test.ts b/packages/rulesets/src/oas/__tests__/array-items.test.ts
new file mode 100644
index 00000000..144ca5d6
--- /dev/null
+++ b/packages/rulesets/src/oas/__tests__/array-items.test.ts
@@ -0,0 +1,81 @@
+import { DiagnosticSeverity } from '@stoplight/types';
+
+import testRule from '../../__tests__/__helpers__/tester';
+
+testRule('array-items', [
+  {
+    name: 'valid case',
+    document: {
+      swagger: '2.0',
+      securityDefinitions: {
+        apikey: {},
+      },
+      paths: {
+        '/path': {
+          get: {
+            security: [
+              {
+                apikey: [],
+              },
+            ],
+          },
+        },
+      },
+    },
+    errors: [],
+  },
+
+  {
+    name: 'array items sibling is present',
+    document: {
+      $ref: '#/',
+      responses: {
+        200: {
+          type: 'array',
+          items: {},
+        },
+        201: {
+          type: 'array',
+          items: {
+            type: 'array',
+            items: {},
+          },
+        },
+      },
+      openapi: '3.0.0',
+    },
+    errors: [],
+  },
+  {
+    name: 'array items sibling is missing',
+    document: {
+      $ref: '#/',
+      responses: {
+        200: {
+          type: 'array',
+        },
+        201: {
+          type: 'array',
+          items: {
+            type: 'array',
+          },
+        },
+      },
+      openapi: '3.0.0',
+    },
+    errors: [
+      {
+        code: 'array-items',
+        message: 'Schemas with "type: array", require a sibling "items" field',
+        path: ['responses', '200'],
+        severity: DiagnosticSeverity.Error,
+      },
+      {
+        code: 'array-items',
+        message: 'Schemas with "type: array", require a sibling "items" field',
+        path: ['responses', '201', 'items'],
+        severity: DiagnosticSeverity.Error,
+      },
+    ],
+  },
+]);
diff --git a/test-harness/README.md b/test-harness/README.md
index 87bfcabb..c27e75eb 100644
--- a/test-harness/README.md
+++ b/test-harness/README.md
@@ -3,7 +3,7 @@
 ## Prerequisites
 
 * Install the project dependencies with `yarn`
-* Generate the binary for your platform with `yarn build.binary`. This will *also* compile the project from TS -> JS
+* Generate the binary for your platform with `yarn workspace @stoplight/spectral-cli build.binary`. This will *also* compile the project from TS -> JS
 
 ## Running the suite
 
@@ -11,11 +11,16 @@ Run `yarn test.harness` from your terminal
 
 ### Running a selected tests
 
-You can run one or selected tests using `TESTS` env variable.
-If you want multiple test files to be run separate them with commas.
-Use paths relative to the `./scenarios` directory.
+Test Harness uses Jest under the hood. You can use all CLI options that Jest supports: https://jestjs.io/docs/cli
 
-E.g. run `TESTS=parameters-ac1.oas2.scenario,validate-body-params/form-byte-format-fail.oas2.scenario yarn test.harness`
+You can run one or multiple tests by passing a path to `test-harness` command.
+All scenarios are converted to `.js` files under the `./tests` directory.
+Hence you must use paths relative to the `./tests` directory, like in the following example:
+
+```bash
+# path to scenario file: `test-harness/scenarios/require-module.scenario`
+yarn test.harness test-harness/tests/require-module
+```
 
 ### Matching test files
 
diff --git a/test-harness/scenarios/require-type-array-items-field.oas3.scenario b/test-harness/scenarios/require-type-array-items-field.oas3.scenario
new file mode 100644
index 00000000..a99facf5
--- /dev/null
+++ b/test-harness/scenarios/require-type-array-items-field.oas3.scenario
@@ -0,0 +1,51 @@
+====test====
+Schemas with "type: array", require a sibling "items" field
+====document====
+openapi: 3.0.3
+info:
+  title: test
+  description: Test specification file
+  version: '1.0'
+  contact:
+    name: John Doe
+    url: 'https://example.com'
+    email: john_doe@example.com
+  license:
+    name: Apache 2.0
+    url: 'https://www.apache.org/licenses/LICENSE-2.0'
+servers:
+  - url: 'http://localhost:3000'
+tags:
+  - name: list-endpoint
+    description: Endpoint for listing objects
+paths:
+  /users:
+    get:
+      summary: List Users
+      operationId: get-users
+      description: List all Users
+      tags:
+        - list-endpoint
+      responses:
+        '200':
+          description: OK
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  favoriteColorSets:
+                    type: array
+                    items:
+                      type: array
+
+====asset:ruleset====
+const { oas } = require('@stoplight/spectral-rulesets');
+module.exports = oas;
+====command====
+{bin} lint {document} --ruleset "{asset:ruleset}"
+====stdout====
+{document}
+ 36:27  error  array-items  Schemas with "type: array", require a sibling "items" field  paths./users.get.responses[200].content.application/json.schema.properties.favoriteColorSets.items
+
+✖ 1 problem (1 error, 0 warnings, 0 infos, 0 hints)
diff --git a/test-harness/src/suite.ts b/test-harness/src/suite.ts
index d6377bf8..c8e73811 100644
--- a/test-harness/src/suite.ts
+++ b/test-harness/src/suite.ts
@@ -19,8 +19,14 @@ if (scenario.command === null) {
     // executing Date() before or after spawnNode were constantly leading to occasional mismatches,
     // as the success of that approach was highly bound to the time spent on the actual spawnNode call
     // this is a tad smarter, because instead of naively hoping the date will match, we try to extract the date from the actual output
-    // this regular expression matches "00:43:59" in "Thu Jul 08 2021 00:43:59 GMT+0200 (Central European Summer Time)"
-    const date = RegExp(escapeRegExp(String(Date())).replace(/(\d\d:){2}\d\d/, '(\\d\\d:){2}\\d\\d'));
+    // this regular expression matches "00:43:59" in "Thu Jul 08 2021 00:43:59 GMT+0200 (Central European Summer Time)".
+    // this regular expression is locale agnostic: it will match "Fri May 31 2024 18:26:32 GMT+0300 (за східноєвропейським літнім часом)"
+    const date = RegExp(
+      escapeRegExp(String(Date()))
+        .replace(/(\d\d:){2}\d\d/, '(\\d\\d:){2}\\d\\d')
+        .replace(/\s\\\(.+\\\)/, '\\s+\\([\\w\\s]+\\)'),
+    );
+
     Reflect.defineProperty(env, 'date', {
       configurable: true,
       enumerable: true,