@redocly/openapi-core 1.21.1 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/lib/config/all.js +14 -1
  3. package/lib/config/minimal.js +11 -1
  4. package/lib/config/recommended-strict.js +11 -1
  5. package/lib/config/recommended.js +11 -1
  6. package/lib/oas-types.js +2 -1
  7. package/lib/rules/arazzo/index.js +22 -2
  8. package/lib/rules/arazzo/parameters-unique.d.ts +2 -0
  9. package/lib/rules/arazzo/parameters-unique.js +32 -0
  10. package/lib/rules/arazzo/requestBody-replacements-unique.d.ts +2 -0
  11. package/lib/rules/arazzo/requestBody-replacements-unique.js +28 -0
  12. package/lib/rules/arazzo/source-description-type.d.ts +2 -0
  13. package/lib/rules/arazzo/source-description-type.js +20 -0
  14. package/lib/rules/arazzo/sourceDescriptions-name-unique.d.ts +2 -0
  15. package/lib/rules/arazzo/sourceDescriptions-name-unique.js +24 -0
  16. package/lib/rules/arazzo/step-onFailure-unique.d.ts +2 -0
  17. package/lib/rules/arazzo/step-onFailure-unique.js +32 -0
  18. package/lib/rules/arazzo/step-onSuccess-unique.d.ts +2 -0
  19. package/lib/rules/arazzo/step-onSuccess-unique.js +32 -0
  20. package/lib/rules/arazzo/stepId-unique.d.ts +2 -0
  21. package/lib/rules/arazzo/stepId-unique.js +26 -0
  22. package/lib/rules/arazzo/workflow-dependsOn.d.ts +2 -0
  23. package/lib/rules/arazzo/workflow-dependsOn.js +56 -0
  24. package/lib/rules/arazzo/workflowId-unique.d.ts +2 -0
  25. package/lib/rules/arazzo/workflowId-unique.js +22 -0
  26. package/lib/rules/spot/parameters-not-in-body.d.ts +2 -0
  27. package/lib/rules/spot/{parameters-no-body-inside-in.js → parameters-not-in-body.js} +3 -3
  28. package/lib/rules/spot/version-enum.d.ts +2 -0
  29. package/lib/rules/spot/version-enum.js +21 -0
  30. package/lib/types/arazzo.js +8 -10
  31. package/lib/types/redocly-yaml.d.ts +1 -1
  32. package/lib/types/redocly-yaml.js +14 -1
  33. package/lib/typings/arazzo.d.ts +2 -0
  34. package/lib/typings/arazzo.js +3 -0
  35. package/lib/utils.d.ts +1 -0
  36. package/lib/utils.js +8 -0
  37. package/package.json +2 -2
  38. package/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap +22 -2
  39. package/src/config/all.ts +14 -1
  40. package/src/config/minimal.ts +11 -1
  41. package/src/config/recommended-strict.ts +11 -1
  42. package/src/config/recommended.ts +11 -1
  43. package/src/oas-types.ts +2 -1
  44. package/src/rules/arazzo/__tests__/{parameters-no-body-inside-in.test.ts → parameters-not-in-body.test.ts} +3 -5
  45. package/src/rules/arazzo/__tests__/parameters-unique.test.ts +114 -0
  46. package/src/rules/arazzo/__tests__/requestBody-replacements-unique.test.ts +109 -0
  47. package/src/rules/arazzo/__tests__/source-description-type.test.ts +80 -0
  48. package/src/rules/arazzo/__tests__/sourceDescription-name-unique.test.ts +79 -0
  49. package/src/rules/arazzo/__tests__/step-onFailure-unique.test.ts +111 -0
  50. package/src/rules/arazzo/__tests__/step-onSuccess-unique.test.ts +111 -0
  51. package/src/rules/arazzo/__tests__/stepId-unique.test.ts +95 -0
  52. package/src/rules/arazzo/__tests__/version-enum.test.ts +76 -0
  53. package/src/rules/arazzo/__tests__/workflow-dependsOn.test.ts +212 -0
  54. package/src/rules/arazzo/__tests__/workflowId-unique.test.ts +90 -0
  55. package/src/rules/arazzo/index.ts +22 -2
  56. package/src/rules/arazzo/parameters-unique.ts +33 -0
  57. package/src/rules/arazzo/requestBody-replacements-unique.ts +28 -0
  58. package/src/rules/arazzo/source-description-type.ts +20 -0
  59. package/src/rules/arazzo/sourceDescriptions-name-unique.ts +23 -0
  60. package/src/rules/arazzo/step-onFailure-unique.ts +33 -0
  61. package/src/rules/arazzo/step-onSuccess-unique.ts +33 -0
  62. package/src/rules/arazzo/stepId-unique.ts +24 -0
  63. package/src/rules/arazzo/workflow-dependsOn.ts +56 -0
  64. package/src/rules/arazzo/workflowId-unique.ts +21 -0
  65. package/src/rules/spot/{parameters-no-body-inside-in.ts → parameters-not-in-body.ts} +1 -1
  66. package/src/rules/spot/version-enum.ts +24 -0
  67. package/src/types/arazzo.ts +8 -10
  68. package/src/types/redocly-yaml.ts +14 -1
  69. package/src/typings/arazzo.ts +4 -0
  70. package/src/utils.ts +8 -0
  71. package/tsconfig.tsbuildinfo +1 -1
  72. package/lib/rules/spot/parameters-no-body-inside-in.d.ts +0 -2
@@ -1,6 +1,16 @@
1
1
  import { Spec } from '../common/spec';
2
2
  import { Assertions } from '../common/assertions';
3
- import { ParametersNoBodyInsideIn } from '../spot/parameters-no-body-inside-in';
3
+ import { ParametersNotInBody } from '../spot/parameters-not-in-body';
4
+ import { SourceDescriptionType } from '../arazzo/source-description-type';
5
+ import { VersionEnum } from '../spot/version-enum';
6
+ import { WorkflowIdUnique } from './workflowId-unique';
7
+ import { StepIdUnique } from './stepId-unique';
8
+ import { SourceDescriptionsNameUnique } from './sourceDescriptions-name-unique';
9
+ import { WorkflowDependsOn } from './workflow-dependsOn';
10
+ import { ParametersUnique } from './parameters-unique';
11
+ import { StepOnSuccessUnique } from './step-onSuccess-unique';
12
+ import { StepOnFailureUnique } from './step-onFailure-unique';
13
+ import { RequestBodyReplacementsUnique } from './requestBody-replacements-unique';
4
14
 
5
15
  import type { ArazzoRule } from '../../visitors';
6
16
  import type { ArazzoRuleSet } from '../../oas-types';
@@ -8,7 +18,17 @@ import type { ArazzoRuleSet } from '../../oas-types';
8
18
  export const rules: ArazzoRuleSet<'built-in'> = {
9
19
  spec: Spec as ArazzoRule,
10
20
  assertions: Assertions as ArazzoRule,
11
- 'parameters-no-body-inside-in': ParametersNoBodyInsideIn as ArazzoRule,
21
+ 'parameters-not-in-body': ParametersNotInBody as ArazzoRule,
22
+ 'sourceDescription-type': SourceDescriptionType as ArazzoRule,
23
+ 'version-enum': VersionEnum as ArazzoRule,
24
+ 'workflowId-unique': WorkflowIdUnique as ArazzoRule,
25
+ 'stepId-unique': StepIdUnique as ArazzoRule,
26
+ 'sourceDescription-name-unique': SourceDescriptionsNameUnique as ArazzoRule,
27
+ 'workflow-dependsOn': WorkflowDependsOn as ArazzoRule,
28
+ 'parameters-unique': ParametersUnique as ArazzoRule,
29
+ 'step-onSuccess-unique': StepOnSuccessUnique as ArazzoRule,
30
+ 'step-onFailure-unique': StepOnFailureUnique as ArazzoRule,
31
+ 'requestBody-replacements-unique': RequestBodyReplacementsUnique as ArazzoRule,
12
32
  };
13
33
 
14
34
  export const preprocessors = {};
@@ -0,0 +1,33 @@
1
+ import type { ArazzoRule } from '../../visitors';
2
+ import type { UserContext } from '../../walk';
3
+
4
+ export const ParametersUnique: ArazzoRule = () => {
5
+ return {
6
+ Parameters: {
7
+ enter(parameters, { report, location }: UserContext) {
8
+ if (!parameters) return;
9
+ const seenParameters = new Set();
10
+
11
+ for (const parameter of parameters) {
12
+ if (seenParameters.has(parameter?.name)) {
13
+ report({
14
+ message: 'The parameter `name` must be unique amongst listed parameters.',
15
+ location: location.child([parameters.indexOf(parameter)]),
16
+ });
17
+ }
18
+
19
+ if (seenParameters.has(parameter?.reference)) {
20
+ report({
21
+ message: 'The parameter `reference` must be unique amongst listed parameters.',
22
+ location: location.child([parameters.indexOf(parameter)]),
23
+ });
24
+ }
25
+
26
+ parameter?.name
27
+ ? seenParameters.add(parameter.name)
28
+ : seenParameters.add(parameter.reference);
29
+ }
30
+ },
31
+ },
32
+ };
33
+ };
@@ -0,0 +1,28 @@
1
+ import type { ArazzoRule } from '../../visitors';
2
+ import type { UserContext } from '../../walk';
3
+
4
+ export const RequestBodyReplacementsUnique: ArazzoRule = () => {
5
+ const seenReplacements = new Set();
6
+
7
+ return {
8
+ RequestBody: {
9
+ enter(requestBody, { report, location }: UserContext) {
10
+ if (!requestBody.replacements) return;
11
+
12
+ for (const replacement of requestBody.replacements) {
13
+ if (seenReplacements.has(replacement.target)) {
14
+ report({
15
+ message: 'Every `replacement` in `requestBody` must be unique.',
16
+ location: location.child([
17
+ 'replacements',
18
+ requestBody.replacements.indexOf(replacement),
19
+ `target`,
20
+ ]),
21
+ });
22
+ }
23
+ seenReplacements.add(replacement.target);
24
+ }
25
+ },
26
+ },
27
+ };
28
+ };
@@ -0,0 +1,20 @@
1
+ import type { ArazzoRule } from '../../visitors';
2
+ import type { UserContext } from '../../walk';
3
+
4
+ export const SourceDescriptionType: ArazzoRule = () => {
5
+ return {
6
+ SourceDescriptions: {
7
+ enter(SourceDescriptions, { report, location }: UserContext) {
8
+ for (const sourceDescription of SourceDescriptions) {
9
+ if (!['openapi', 'arazzo'].includes(sourceDescription?.type)) {
10
+ report({
11
+ message:
12
+ 'The `type` property of the `sourceDescription` object must be either `openapi` or `arazzo`.',
13
+ location: location.child([SourceDescriptions.indexOf(sourceDescription)]),
14
+ });
15
+ }
16
+ }
17
+ },
18
+ },
19
+ };
20
+ };
@@ -0,0 +1,23 @@
1
+ import type { ArazzoRule } from '../../visitors';
2
+ import type { UserContext } from '../../walk';
3
+
4
+ export const SourceDescriptionsNameUnique: ArazzoRule = () => {
5
+ const seenSourceDescriptions = new Set();
6
+
7
+ return {
8
+ SourceDescriptions: {
9
+ enter(sourceDescriptions, { report, location }: UserContext) {
10
+ if (!sourceDescriptions.length) return;
11
+ for (const sourceDescription of sourceDescriptions) {
12
+ if (seenSourceDescriptions.has(sourceDescription.name)) {
13
+ report({
14
+ message: 'The `name` must be unique amongst all SourceDescriptions.',
15
+ location: location.child([sourceDescriptions.indexOf(sourceDescription)]),
16
+ });
17
+ }
18
+ seenSourceDescriptions.add(sourceDescription.name);
19
+ }
20
+ },
21
+ },
22
+ };
23
+ };
@@ -0,0 +1,33 @@
1
+ import type { ArazzoRule } from '../../visitors';
2
+ import type { UserContext } from '../../walk';
3
+
4
+ export const StepOnFailureUnique: ArazzoRule = () => {
5
+ return {
6
+ OnFailureActionList: {
7
+ enter(onFailureActionList, { report, location }: UserContext) {
8
+ if (!onFailureActionList) return;
9
+ const seenFailureActions = new Set();
10
+
11
+ for (const onFailureAction of onFailureActionList) {
12
+ if (seenFailureActions.has(onFailureAction?.name)) {
13
+ report({
14
+ message: 'The action `name` must be unique amongst listed `onFailure` actions.',
15
+ location: location.child([onFailureActionList.indexOf(onFailureAction)]),
16
+ });
17
+ }
18
+
19
+ if (seenFailureActions.has(onFailureAction?.reference)) {
20
+ report({
21
+ message: 'The action `reference` must be unique amongst listed `onFailure` actions.',
22
+ location: location.child([onFailureActionList.indexOf(onFailureAction)]),
23
+ });
24
+ }
25
+
26
+ onFailureAction?.name
27
+ ? seenFailureActions.add(onFailureAction.name)
28
+ : seenFailureActions.add(onFailureAction.reference);
29
+ }
30
+ },
31
+ },
32
+ };
33
+ };
@@ -0,0 +1,33 @@
1
+ import type { ArazzoRule } from '../../visitors';
2
+ import type { UserContext } from '../../walk';
3
+
4
+ export const StepOnSuccessUnique: ArazzoRule = () => {
5
+ return {
6
+ OnSuccessActionList: {
7
+ enter(onSuccessActionList, { report, location }: UserContext) {
8
+ if (!onSuccessActionList) return;
9
+ const seenSuccessActions = new Set();
10
+
11
+ for (const onSuccessAction of onSuccessActionList) {
12
+ if (seenSuccessActions.has(onSuccessAction?.name)) {
13
+ report({
14
+ message: 'The action `name` must be unique amongst listed `onSuccess` actions.',
15
+ location: location.child([onSuccessActionList.indexOf(onSuccessAction)]),
16
+ });
17
+ }
18
+
19
+ if (seenSuccessActions.has(onSuccessAction?.reference)) {
20
+ report({
21
+ message: 'The action `reference` must be unique amongst listed `onSuccess` actions.',
22
+ location: location.child([onSuccessActionList.indexOf(onSuccessAction)]),
23
+ });
24
+ }
25
+
26
+ onSuccessAction?.name
27
+ ? seenSuccessActions.add(onSuccessAction.name)
28
+ : seenSuccessActions.add(onSuccessAction.reference);
29
+ }
30
+ },
31
+ },
32
+ };
33
+ };
@@ -0,0 +1,24 @@
1
+ import type { ArazzoRule } from '../../visitors';
2
+ import type { UserContext } from '../../walk';
3
+
4
+ export const StepIdUnique: ArazzoRule = () => {
5
+ return {
6
+ Workflow: {
7
+ enter(workflow, { report, location }: UserContext) {
8
+ if (!workflow.steps) return;
9
+ const seenSteps = new Set();
10
+
11
+ for (const step of workflow.steps) {
12
+ if (!step.stepId) return;
13
+ if (seenSteps.has(step.stepId)) {
14
+ report({
15
+ message: 'The `stepId` must be unique amongst all steps described in the workflow.',
16
+ location: location.child(['steps', workflow.steps.indexOf(step)]),
17
+ });
18
+ }
19
+ seenSteps.add(step.stepId);
20
+ }
21
+ },
22
+ },
23
+ };
24
+ };
@@ -0,0 +1,56 @@
1
+ import type { ArazzoRule } from '../../visitors';
2
+ import type { UserContext } from '../../walk';
3
+
4
+ export const WorkflowDependsOn: ArazzoRule = () => {
5
+ const seenWorkflow = new Set();
6
+ const existingSourceDescriptions = new Set();
7
+ const existingWorkflowIds = new Set();
8
+
9
+ return {
10
+ SourceDescriptions: {
11
+ enter(sourceDescriptions) {
12
+ for (const sourceDescription of sourceDescriptions) {
13
+ existingSourceDescriptions.add(sourceDescription.name);
14
+ }
15
+ },
16
+ },
17
+ Workflows: {
18
+ enter(workflows) {
19
+ for (const workflow of workflows) {
20
+ existingWorkflowIds.add(workflow.workflowId);
21
+ }
22
+ },
23
+ },
24
+ Workflow: {
25
+ leave(workflow, { report, location }: UserContext) {
26
+ if (!workflow.dependsOn) return;
27
+
28
+ for (const item of workflow.dependsOn) {
29
+ // Possible dependsOn workflow pattern: $sourceDescriptions.<name>.<workflowId>
30
+ if (item.startsWith('$sourceDescriptions.')) {
31
+ const sourceDescriptionName = item.split('.')[1];
32
+ if (!existingSourceDescriptions.has(sourceDescriptionName)) {
33
+ report({
34
+ message: `SourceDescription ${sourceDescriptionName} must be defined in sourceDescriptions.`,
35
+ location: location.child([`dependsOn`, workflow.dependsOn.indexOf(item)]),
36
+ });
37
+ }
38
+ }
39
+ if (!item.startsWith('$sourceDescriptions') && !existingWorkflowIds.has(item)) {
40
+ report({
41
+ message: `Workflow ${item} must be defined in workflows.`,
42
+ location: location.child([`dependsOn`, workflow.dependsOn.indexOf(item)]),
43
+ });
44
+ }
45
+ if (seenWorkflow.has(item)) {
46
+ report({
47
+ message: 'Every workflow in dependsOn must be unique.',
48
+ location: location.child([`dependsOn`]),
49
+ });
50
+ }
51
+ seenWorkflow.add(item);
52
+ }
53
+ },
54
+ },
55
+ };
56
+ };
@@ -0,0 +1,21 @@
1
+ import type { ArazzoRule } from '../../visitors';
2
+ import type { UserContext } from '../../walk';
3
+
4
+ export const WorkflowIdUnique: ArazzoRule = () => {
5
+ const seenWorkflow = new Set();
6
+
7
+ return {
8
+ Workflow: {
9
+ enter(workflow, { report, location }: UserContext) {
10
+ if (!workflow.workflowId) return;
11
+ if (seenWorkflow.has(workflow.workflowId)) {
12
+ report({
13
+ message: 'Every workflow must have a unique `workflowId`.',
14
+ location: location.child([workflow.workflowId]),
15
+ });
16
+ }
17
+ seenWorkflow.add(workflow.workflowId);
18
+ },
19
+ },
20
+ };
21
+ };
@@ -1,7 +1,7 @@
1
1
  import type { ArazzoRule } from '../../visitors';
2
2
  import type { UserContext } from '../../walk';
3
3
 
4
- export const ParametersNoBodyInsideIn: ArazzoRule = () => {
4
+ export const ParametersNotInBody: ArazzoRule = () => {
5
5
  return {
6
6
  Parameter: {
7
7
  enter(parameter, { report, location }: UserContext) {
@@ -0,0 +1,24 @@
1
+ import { ARAZZO_VERSIONS_SUPPORTED_BY_SPOT } from '../../typings/arazzo';
2
+ import { pluralize } from '../../utils';
3
+
4
+ import type { ArazzoRule } from '../../visitors';
5
+ import type { UserContext } from '../../walk';
6
+
7
+ export const VersionEnum: ArazzoRule = () => {
8
+ const supportedVersions = ARAZZO_VERSIONS_SUPPORTED_BY_SPOT.join(', ');
9
+ return {
10
+ Root: {
11
+ enter(root, { report, location }: UserContext) {
12
+ if (!ARAZZO_VERSIONS_SUPPORTED_BY_SPOT.includes(root.arazzo)) {
13
+ report({
14
+ message: `Only ${supportedVersions} Arazzo ${pluralize(
15
+ 'version is',
16
+ ARAZZO_VERSIONS_SUPPORTED_BY_SPOT.length
17
+ )} supported by Spot.`,
18
+ location: location.child('arazzo'),
19
+ });
20
+ }
21
+ },
22
+ },
23
+ };
24
+ };
@@ -4,7 +4,7 @@ import { Discriminator, DiscriminatorMapping, ExternalDocs, Xml } from './oas3';
4
4
 
5
5
  const Root: NodeType = {
6
6
  properties: {
7
- arazzo: { type: 'string', enum: ['1.0.0'] },
7
+ arazzo: { type: 'string' },
8
8
  info: 'Info',
9
9
  sourceDescriptions: 'SourceDescriptions',
10
10
  'x-parameters': 'Parameters',
@@ -89,9 +89,7 @@ const ArazzoSourceDescription: NodeType = {
89
89
  const ReusableObject: NodeType = {
90
90
  properties: {
91
91
  reference: { type: 'string' },
92
- value: {
93
- type: 'string',
94
- },
92
+ value: {}, // any
95
93
  },
96
94
  required: ['reference'],
97
95
  extensionsPrefix: 'x-',
@@ -108,10 +106,10 @@ const Parameter: NodeType = {
108
106
  const Parameters: NodeType = {
109
107
  properties: {},
110
108
  items: (value: any) => {
111
- if (value?.in) {
112
- return 'Parameter';
113
- } else {
109
+ if (value?.reference) {
114
110
  return 'ReusableObject';
111
+ } else {
112
+ return 'Parameter';
115
113
  }
116
114
  },
117
115
  };
@@ -122,7 +120,7 @@ const Workflow: NodeType = {
122
120
  description: { type: 'string' },
123
121
  parameters: 'Parameters',
124
122
  dependsOn: { type: 'array', items: { type: 'string' } },
125
- inputs: 'NamedInputs',
123
+ inputs: 'Schema',
126
124
  outputs: 'Outputs',
127
125
  steps: 'Steps',
128
126
  successActions: 'OnSuccessActionList',
@@ -234,7 +232,7 @@ const SuccessActionObject: NodeType = {
234
232
  type: { type: 'string', enum: ['goto', 'end'] },
235
233
  stepId: { type: 'string' },
236
234
  workflowId: { type: 'string' },
237
- criteria: 'CriterionObject',
235
+ criteria: listOf('CriterionObject'),
238
236
  },
239
237
  required: ['type', 'name'],
240
238
  };
@@ -256,7 +254,7 @@ const FailureActionObject: NodeType = {
256
254
  stepId: { type: 'string' },
257
255
  retryAfter: { type: 'number' },
258
256
  retryLimit: { type: 'number' },
259
- criteria: 'CriterionObject',
257
+ criteria: listOf('CriterionObject'),
260
258
  },
261
259
  required: ['type', 'name'],
262
260
  };
@@ -109,7 +109,20 @@ export type BuiltInAsync2RuleId = typeof builtInAsync2Rules[number];
109
109
 
110
110
  export type BuiltInAsync3RuleId = typeof builtInAsync3Rules[number];
111
111
 
112
- const builtInArazzoRules = ['spec', 'parameters-no-body-inside-in'] as const;
112
+ const builtInArazzoRules = [
113
+ 'spec',
114
+ 'parameters-not-in-body',
115
+ 'sourceDescription-type',
116
+ 'version-enum',
117
+ 'workflowId-unique',
118
+ 'stepId-unique',
119
+ 'sourceDescription-name-unique',
120
+ 'workflow-dependsOn',
121
+ 'parameters-unique',
122
+ 'step-onSuccess-unique',
123
+ 'step-onFailure-unique',
124
+ 'requestBody-replacements-unique',
125
+ ] as const;
113
126
 
114
127
  export type BuiltInArazzoRuleId = typeof builtInArazzoRules[number];
115
128
 
@@ -170,3 +170,7 @@ export interface ArazzoDefinition {
170
170
  };
171
171
  };
172
172
  }
173
+
174
+ export const VERSION_PATTERN = /^1\.0\.\d+(-.+)?$/;
175
+
176
+ export const ARAZZO_VERSIONS_SUPPORTED_BY_SPOT = ['1.0.0'];
package/src/utils.ts CHANGED
@@ -6,6 +6,7 @@ import { parseYaml } from './js-yaml';
6
6
  import { env } from './env';
7
7
  import { logger, colorize } from './logger';
8
8
  import { HttpsProxyAgent } from 'https-proxy-agent';
9
+ import * as pluralizeOne from 'pluralize';
9
10
 
10
11
  import type { HttpResolveConfig } from './config';
11
12
  import type { UserContext } from './walk';
@@ -23,6 +24,13 @@ export function pushStack<T, P extends Stack<T> = Stack<T>>(head: P, value: T) {
23
24
  return { prev: head, value };
24
25
  }
25
26
 
27
+ export function pluralize(sentence: string, count?: number, inclusive?: boolean) {
28
+ return sentence
29
+ .split(' ')
30
+ .map((word) => pluralizeOne(word, count, inclusive))
31
+ .join(' ');
32
+ }
33
+
26
34
  export function popStack<T, P extends Stack<T>>(head: P) {
27
35
  return head?.prev ?? null;
28
36
  }