Skip to content

Commit

Permalink
feat(ruleset): introduce parser options
Browse files Browse the repository at this point in the history
  • Loading branch information
P0lip committed Sep 30, 2020
1 parent 5d49020 commit bf07ef1
Show file tree
Hide file tree
Showing 15 changed files with 299 additions and 48 deletions.
13 changes: 13 additions & 0 deletions docs/guides/4-custom-rulesets.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,19 @@ message: "{{value}} is greater than 0"
message: "{{path}} cannot point at remote reference"
```

## Parsing Options

If you do not care about duplicate keys or invalid values (such as non-string mapping keys in YAML), you can tune their severity using `parserOptions` setting.

```yaml
extends: spectral:oas
parserOptions:
duplicateKeys: warn # error is the default value
incompatibleValues: off # error is the default value
```

`parserOptions` is not inherited by extended rulesets.

## Documentation URL

Optionally provide a documentation URL to your ruleset in order to help end-users find more information about various warnings. Result messages will sometimes be more than enough to explain what the problem is, but it can also be beneficial to explain _why_ a message exists, and this is a great place to do that.
Expand Down
137 changes: 136 additions & 1 deletion src/__tests__/linter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { mergeRules, readRuleset } from '../rulesets';
import { RuleCollection, Spectral } from '../spectral';
import { httpAndFileResolver } from '../resolvers/http-and-file';
import { Parsers, Document } from '..';
import { IParser } from '../parsers/types';

const invalidSchema = JSON.stringify(require('./__fixtures__/petstore.invalid-schema.oas3.json'));
const studioFixture = JSON.stringify(require('./__fixtures__/studio-default-fixture-oas3.json'), null, 2);
Expand Down Expand Up @@ -856,11 +857,145 @@ responses:: !!foo
line: 1,
},
},
severity: DiagnosticSeverity.Warning,
severity: DiagnosticSeverity.Error,
},
]);
});

describe('parser options', () => {
test('should allow changing the severity of invalid YAML mapping keys diagnostics', async () => {
spectral.setRuleset({
rules: {},
functions: {},
exceptions: {},
parserOptions: {
incompatibleValues: 'info',
},
});
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.Information,
},
]);
});

test('should allow disabling invalid YAML mapping keys diagnostics', async () => {
spectral.setRuleset({
rules: {},
functions: {},
exceptions: {},
parserOptions: {
incompatibleValues: 'off',
},
});
const results = await spectral.run(
`responses:
200:
description: ''
500:
'400':
description: ''`,
{ ignoreUnknownFormat: true },
);

expect(results).toEqual([]);
});

test.each<keyof typeof Parsers>(['Json', 'Yaml'])(
'should allow changing the severity of duplicate key diagnostics reported by %s parser',
async parser => {
spectral.setRuleset({
rules: {},
functions: {},
exceptions: {},
parserOptions: {
duplicateKeys: 'info',
},
});

const results = await spectral.run(
new Document(
`{
"200": {},
"200": {}
}`,
Parsers[parser] as IParser,
),
{ ignoreUnknownFormat: true },
);

expect(results).toEqual([
{
code: 'parser',
message: 'Duplicate key: 200',
path: ['200'],
range: {
end: {
character: 7,
line: 2,
},
start: {
character: 2,
line: 2,
},
},
severity: DiagnosticSeverity.Information,
},
]);
},
);

test.each<keyof typeof Parsers>(['Json', 'Yaml'])(
'should allow disabling duplicate key diagnostics reported by %s parser',
async parser => {
spectral.setRuleset({
rules: {},
functions: {},
exceptions: {},
parserOptions: {
duplicateKeys: 'off',
},
});

const results = await spectral.run(
new Document(
`{
"200": {},
"200": {},
"200": {}
}`,
Parsers[parser] as IParser,
),
{ ignoreUnknownFormat: true },
);

expect(results).toEqual([]);
},
);
});

describe('functional tests for the given property', () => {
let fakeLintingFunction: any;

Expand Down
8 changes: 8 additions & 0 deletions src/consts.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import { IParserOptions } from './types/ruleset';
import { DiagnosticSeverity } from '@stoplight/types';

export const NPM_PKG_ROOT = 'https://unpkg.com/';
export const SPECTRAL_PKG_NAME = '@stoplight/spectral';
export const SPECTRAL_PKG_VERSION = '';

export const DEFAULT_PARSER_OPTIONS = Object.freeze<Required<IParserOptions>>({
incompatibleValues: DiagnosticSeverity.Error,
duplicateKeys: DiagnosticSeverity.Error,
});
4 changes: 2 additions & 2 deletions src/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function normalizeSource(source: Optional<string>): string | null {
export class Document<D = unknown, R extends IParserResult = IParserResult<D>> implements IDocument<D> {
protected readonly parserResult: R;
public readonly source: string | null;
public readonly diagnostics: ReadonlyArray<IRuleResult>;
public readonly diagnostics: IRuleResult[];
public formats?: string[] | null;

constructor(protected readonly input: string, protected readonly parser: IParser<R>, source?: string) {
Expand Down Expand Up @@ -66,7 +66,7 @@ export class Document<D = unknown, R extends IParserResult = IParserResult<D>> i

export class ParsedDocument<D = unknown, R extends IParsedResult = IParsedResult> implements IDocument<D> {
public readonly source: string | null;
public readonly diagnostics: ReadonlyArray<IRuleResult>;
public readonly diagnostics: IRuleResult[];
public formats?: string[] | null;

constructor(protected readonly parserResult: R) {
Expand Down
6 changes: 3 additions & 3 deletions src/errorMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { IRuleResult } from './types';
const toUpperCase = (word: string) => word.toUpperCase();
const splitWord = (word: string, end: string, start: string) => `${end} ${start.toLowerCase()}`;

export function getDiagnosticErrorMessage(diagnostic: IDiagnostic) {
export function getDiagnosticErrorMessage(diagnostic: IDiagnostic): string {
const key = getPropertyKey(diagnostic.path);
let prettifiedMessage = diagnostic.message.replace(/^[a-z]/, toUpperCase);

Expand Down Expand Up @@ -37,7 +37,7 @@ export function formatParserDiagnostics(diagnostics: ReadonlyArray<IDiagnostic>,
code: 'parser',
message: getDiagnosticErrorMessage(diagnostic),
path: diagnostic.path ?? [],
...(source !== null && { source }),
...(source !== null ? { source } : null),
}));
}

Expand All @@ -53,7 +53,7 @@ export const formatResolverErrors = (document: IDocument, diagnostics: IResolveE
message: prettyPrintResolverErrorMessage(error.message),
severity: DiagnosticSeverity.Error,
range,
...(source !== null && { source }),
...(source !== null ? { source } : null),
};
});
};
31 changes: 2 additions & 29 deletions src/meta/rule.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -341,35 +341,8 @@
],
"type": "object"
},
"DiagnosticSeverity": {
"enum": [
-1,
0,
1,
2,
3
],
"type": "number"
},
"HumanReadableSeverity": {
"type": "string",
"enum": [
"error",
"warn",
"info",
"hint",
"off"
]
},
"Severity": {
"oneOf": [
{
"$ref": "#/definitions/DiagnosticSeverity"
},
{
"$ref": "#/definitions/HumanReadableSeverity"
}
]
"$ref": "shared.json#/Severity"
}
},
"oneOf": [
Expand Down Expand Up @@ -444,7 +417,7 @@
"type": "object"
},
{
"$ref": "#/definitions/HumanReadableSeverity"
"$ref": "shared.json#/HumanReadableSeverity"
},
{
"type": "boolean"
Expand Down
12 changes: 12 additions & 0 deletions src/meta/ruleset.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@
"type": "string"
}
}
},
"parserOptions": {
"type": "object",
"properties": {
"duplicateKeys": {
"$ref": "shared.json#/Severity"
},
"incompatibleValues": {
"$ref": "shared.json#/Severity"
}
},
"additionalProperties": false
}
},
"anyOf": [
Expand Down
19 changes: 19 additions & 0 deletions src/meta/shared.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"$id": "http://stoplight.io/schemas/shared.json",
"DiagnosticSeverity": {
"enum": [-1, 0, 1, 2, 3]
},
"HumanReadableSeverity": {
"enum": ["error", "warn", "info", "hint", "off"]
},
"Severity": {
"oneOf": [
{
"$ref": "#/DiagnosticSeverity"
},
{
"$ref": "#/HumanReadableSeverity"
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": [],
"parserOptions": {
"incompatibleValues": "off",
"duplicateKeys": "warn"
}
}
9 changes: 9 additions & 0 deletions src/rulesets/__tests__/reader.jest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,15 @@ describe('Rulesets reader', () => {
expect(rules['oas3-valid-rule'].formats).toEqual(['oas3']);
});

it('should include parserOptions', async () => {
const { parserOptions } = await readRuleset(path.join(__dirname, '__fixtures__/parser-options-ruleset.json'));

expect(parserOptions).toStrictEqual({
duplicateKeys: 'warn',
incompatibleValues: 'off',
});
});

it('given spectral:oas ruleset, should not pick up unrecommended rules', () => {
return expect(readRuleset('spectral:oas')).resolves.toEqual(
expect.objectContaining({
Expand Down
35 changes: 35 additions & 0 deletions src/rulesets/__tests__/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,41 @@ describe('Ruleset Validation', () => {
});
});
});

describe('parser options validation', () => {
it('recognizes valid options', () => {
expect(
assertValidRuleset.bind(null, {
extends: [],
parserOptions: {
incompatibleValues: 'warn',
},
}),
).not.toThrow();

expect(
assertValidRuleset.bind(null, {
extends: [],
parserOptions: {
incompatibleValues: 2,
duplicateKeys: 'hint',
},
}),
).not.toThrow();
});

it('given invalid values, throws', () => {
expect(
assertValidRuleset.bind(null, {
extends: [],
parserOptions: {
incompatibleValues: 5,
duplicateKeys: 'foo',
},
}),
).toThrow(ValidationError);
});
});
});

describe('Function Validation', () => {
Expand Down
Loading

0 comments on commit bf07ef1

Please sign in to comment.