Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,16 @@ module.exports = {
parser: 'yaml-eslint-parser',
plugins: ["automation-custom"],
rules: {
'@typescript-eslint/naming-convention': 0,
'yml/plain-scalar': [
2,
"always"
, {
// ignore path from ref, that must be quoted
ignorePatterns: [
'[./#a-zA-Z0-9_]+'
]
}
],
'yml/quotes': [
2,
{
Expand All @@ -35,7 +44,16 @@ module.exports = {
files: ['specs/**/*.yml'],
rules: {
"automation-custom/description-dot": "error",
}
"automation-custom/single-quote-ref": "error",
},
overrides: [
{
files: ['!specs/bundled/*.yml'],
rules: {
"automation-custom/out-of-line-enum": "error",
}
}
]
}
]
},
Expand Down
2 changes: 1 addition & 1 deletion .github/.cache_version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
7.1.0
7.2.0
4 changes: 4 additions & 0 deletions eslint/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { descriptionDot } from './rules/descriptionDot';
import { outOfLineEnum } from './rules/outOfLineEnum';
import { singleQuoteRef } from './rules/singleQuoteRef';

const rules = {
'description-dot': descriptionDot,
'out-of-line-enum': outOfLineEnum,
'single-quote-ref': singleQuoteRef,
};

export { rules };
44 changes: 44 additions & 0 deletions eslint/src/rules/outOfLineEnum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Rule } from 'eslint';

import { isPairWithKey } from '../utils';

export const outOfLineEnum: Rule.RuleModule = {
meta: {
docs: {
description: 'enum must be out of line',
},
messages: {
enumNotOutOfLine: 'enum is not out of line',
},
},
create(context) {
if (!context.parserServices.isYAML) {
return {};
}

return {
YAMLPair(node): void {
if (!isPairWithKey(node, 'enum')) {
return;
}
// parent is mapping, and parent is real parent that must be to the far left
if (node.parent.parent.loc.start.column === 0) {
return;
}
if (
isPairWithKey(
node.parent.parent.parent.parent?.parent?.parent?.parent ?? null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way to prevent that kind of path ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a very specific edge case that we want to solve here, it won't be used anywhere else but if we need to we can have a function that checks N parents above, I will add it if I ever encounter a parent chain I will add it !

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can maybe have a function that will find the deepest parent in a given element, so it handles even deeper ones etc.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it's not necessarily the deepest, it's just that's the 7th parent has be to a pair with key servers, not sure if it generalizable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but there should only be one servers in a spec so we can maybe assume that the deepest or first found is correct?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could, but that won't make the code simpler so I don't see why change

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was more to make sure if it's the 6th or 9th it won't require an other manual check, it's not necessary to change it right now, we can iterate on it if there's an issue one day

'servers'
)
) {
// accept out of line enum if in servers
return;
}
context.report({
node: node.parent.parent as any,
messageId: 'enumNotOutOfLine',
});
},
};
},
};
55 changes: 55 additions & 0 deletions eslint/src/rules/singleQuoteRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Rule } from 'eslint';

import { isBLockScalar, isPairWithKey, isScalar } from '../utils';

export const singleQuoteRef: Rule.RuleModule = {
meta: {
docs: {
description: '$ref must be wrapped in single quote',
},
messages: {
refNoQuote: '$ref is not wrapped in single quote',
},
fixable: 'code',
},
create(context) {
if (!context.parserServices.isYAML) {
return {};
}

return {
YAMLPair(node): void {
if (!isPairWithKey(node, '$ref')) {
return;
}
if (!isScalar(node.value)) {
// not our problem, something else will fail like path resolution
return;
}
if (node.value.style === 'single-quoted') {
// that's what we want
return;
}
if (isBLockScalar(node.value)) {
// another rule should take care of that case
return;
}
const hasDoubleQuote = node.value.style === 'double-quoted';
const [start, end] = node.value.range;
context.report({
node: node as any,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how come we need to cast 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an issue between eslint type Node and yaml-eslint-parser type that inherits from the wrong Node, so type are not compatible but in practice they have the same properties.

messageId: 'refNoQuote',
*fix(fixer) {
if (hasDoubleQuote) {
yield fixer.replaceTextRange([start, start + 1], "'");
yield fixer.replaceTextRange([end - 1, end], "'");
} else {
yield fixer.insertTextBeforeRange([start, start], "'");
yield fixer.insertTextAfterRange([end, end], "'");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd check if the last character is not ' , otherwise it will escape the last you add ;) . (in the case it doesn't start with a quote but it ends with it)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if I saw it elsewhere but we also need to make sure the string does not contain a ', if so we should fallback to double quoted

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pretty sure " and ' are forbidden characters for path and yaml key, this is not a concern for this particular rule.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes this is only for refs sorry I forgot I was thinking of description/summaries

}
},
});
},
};
},
};
66 changes: 66 additions & 0 deletions eslint/tests/outOfLineEnum.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { RuleTester } from 'eslint';

import { outOfLineEnum } from '../src/rules/outOfLineEnum';

const ruleTester = new RuleTester({
parser: require.resolve('yaml-eslint-parser'),
});

ruleTester.run('out-of-line-enum', outOfLineEnum, {
valid: [
`
simple:
type: string
enum: [bla, blabla]
`,
`
simple:
type: string
enum:
- bla
- blabla
`,
`
simple:
type: string
enum: [bla, blabla]

useIt:
$ref: '#/simple'
`,
`
servers:
- url: http://test-server.com
variables:
region:
default: us
enum:
- us
- de
`,
],
invalid: [
{
code: `
root:
inside:
type: string
enum: [bla, blabla]
`,
errors: [{ messageId: 'enumNotOutOfLine' }],
},
{
code: `
root:
inside:
deeper:
type: string
enum: [bla, blabla]

useIt:
$ref: '#/root/inside/deeper'
`,
errors: [{ messageId: 'enumNotOutOfLine' }],
},
],
});
82 changes: 82 additions & 0 deletions eslint/tests/singleQuoteRef.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { RuleTester } from 'eslint';

import { singleQuoteRef } from '../src/rules/singleQuoteRef';

const ruleTester = new RuleTester({
parser: require.resolve('yaml-eslint-parser'),
});

ruleTester.run('single-quote-ref', singleQuoteRef, {
valid: [
`
simple:
$ref: 'random/path.yml'`,
`
sameFile:
$ref: '#/inside'`,
`
long:
$ref: 'some/random/file.yml#/root/object/prop'
`,
`
post:
description: test desc.
'400':
$ref: '../../common/responses/BadRequest.yml'
`,
],
invalid: [
{
code: `
simple:
$ref: random/path.yml
`,
errors: [{ messageId: 'refNoQuote' }],
output: `
simple:
$ref: 'random/path.yml'
`,
},
{
code: `
long:
$ref: some/random/file.yml#/root/object/prop
`,
errors: [{ messageId: 'refNoQuote' }],
output: `
long:
$ref: 'some/random/file.yml#/root/object/prop'
`,
},
{
code: `
post:
description: test desc.
'400':
$ref: ../../common/responses/BadRequest.yml
'404':
$ref: ../../common/responses/IndexNotFound.yml
`,
errors: [{ messageId: 'refNoQuote' }, { messageId: 'refNoQuote' }],
output: `
post:
description: test desc.
'400':
$ref: '../../common/responses/BadRequest.yml'
'404':
$ref: '../../common/responses/IndexNotFound.yml'
`,
},
{
code: `
double:
$ref: "some/ref"
`,
errors: [{ messageId: 'refNoQuote' }],
output: `
double:
$ref: 'some/ref'
`,
},
],
});
37 changes: 36 additions & 1 deletion scripts/buildSpecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,39 @@ async function propagateTagsToOperations(
return true;
}

async function lintCommon(verbose: boolean, useCache: boolean): Promise<void> {
let hash = '';
const cacheFile = toAbsolutePath(`specs/dist/common.cache`);
if (useCache) {
const { cacheExists, hash: newCache } = await checkForCache(
{
job: 'common specs',
folder: toAbsolutePath('specs/'),
generatedFiles: [],
filesToCache: ['common'],
cacheFile,
},
verbose
);

if (cacheExists) {
return;
}

hash = newCache;
}

const spinner = createSpinner('linting common spec', verbose).start();
await run(`yarn specs:lint common`, { verbose });
Comment on lines +66 to +67
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't we want to fix when it's not the CI?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of scripts modifying hand written file when it's unclear, but here it should be fine

Copy link
Member

@shortcuts shortcuts Apr 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense but if it's only for dev purposes it's fine I guess

(as you want)


if (hash) {
spinner.text = `storing common spec cache`;
await fsp.writeFile(cacheFile, hash);
}

spinner.succeed();
}

async function buildSpec(
client: string,
outputFormat: string,
Expand Down Expand Up @@ -87,7 +120,7 @@ async function buildSpec(
}

spinner.text = `linting ${client} spec`;
await run(`yarn specs:lint ${client}`, { verbose });
await run(`yarn specs:fix ${client}`, { verbose });

spinner.text = `validating ${client} spec`;
await run(`yarn openapi lint specs/bundled/${client}.${outputFormat}`, {
Expand All @@ -113,6 +146,8 @@ export async function buildSpecs(
): Promise<void> {
await fsp.mkdir(toAbsolutePath('specs/dist'), { recursive: true });

await lintCommon(verbose, useCache);

await Promise.all(
clients.map((client) => buildSpec(client, outputFormat, verbose, useCache))
);
Expand Down
10 changes: 5 additions & 5 deletions specs/abtesting/common/schemas/ABTest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ ABTest:
additionalProperties: false
properties:
abTestID:
$ref: ../parameters.yml#/abTestID
$ref: '../parameters.yml#/abTestID'
clickSignificance:
type: number
format: double
Expand All @@ -19,16 +19,16 @@ ABTest:
format: double
description: A/B test significance based on conversion data. Should be > 0.95 to be considered significant (no matter which variant is winning).
endAt:
$ref: ../parameters.yml#/endAt
$ref: '../parameters.yml#/endAt'
createdAt:
$ref: ../parameters.yml#/createdAt
$ref: '../parameters.yml#/createdAt'
name:
$ref: ../parameters.yml#/name
$ref: '../parameters.yml#/name'
status:
type: string
description: status of the A/B test.
variants:
$ref: Variant.yml#/variants
$ref: 'Variant.yml#/variants'
required:
- status
- name
Expand Down
Loading