diff --git a/CHANGELOG.md b/CHANGELOG.md index c060f91d..1d86095c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Added syntax rule for `None` literal + +### Fixed +- Profile header rule now strictly requires full semver version in version field, as the spec defines +- Condition to add SingleLineCommentTrivia to termina tokens in sublexer ## [2.0.0] - 2022-11-08 ### Added diff --git a/README.md b/README.md index 80d8fcde..843a976d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Parser -![GitHub Workflow Status](https://img.shields.io/github/workflow/status/superfaceai/parser/CI) +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/superfaceai/parser/main.yml?branch=dev)](https://github.com/superfaceai/parser/actions/workflows/main.yml) ![NPM](https://img.shields.io/npm/v/@superfaceai/parser) [![NPM](https://img.shields.io/npm/l/@superfaceai/parser)](LICENSE) ![TypeScript](https://img.shields.io/badge/%3C%2F%3E-Typescript-blue) diff --git a/package.json b/package.json index 6be2a338..85d96a71 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@superfaceai/parser", - "version": "2.0.0", + "version": "2.1.0-rc.0", "description": "Level 5 autonomous, self-driving API client, https://superface.ai", "repository": "https://github.com/superfaceai/parser.git", "source": "lib/index.js", @@ -43,7 +43,7 @@ "ts-jest": "^27.1.3" }, "dependencies": { - "@superfaceai/ast": "^1.2.0", + "@superfaceai/ast": "^1.3.0", "@types/debug": "^4.1.5", "debug": "^4.3.3", "typescript": "^4" diff --git a/src/interpreter/example-validator.test.ts b/src/interpreter/example-validator.test.ts index 13e4746f..d98d59b2 100644 --- a/src/interpreter/example-validator.test.ts +++ b/src/interpreter/example-validator.test.ts @@ -99,5 +99,25 @@ describe('ExampleValidator', () => { 'ComlinkPrimitiveLiteral - Wrong Structure: expected [boolean | number], but got "string"', ]); }); + + it('none', () => { + const profileAst = parseProfileFromSource( + `usecase Test { + result { + f1 string! + } + + example fail { + result { + f1 = None + } + } + }` + ); + + expect(profileAst).not.toBeValidExample([ + 'ComlinkNoneLiteral - Wrong Structure: expected string!, but got None', + ]); + }); }); }); diff --git a/src/interpreter/example-validator.ts b/src/interpreter/example-validator.ts index 1a5e8036..a329d8c2 100644 --- a/src/interpreter/example-validator.ts +++ b/src/interpreter/example-validator.ts @@ -2,6 +2,7 @@ import { ComlinkAssignmentNode, ComlinkListLiteralNode, ComlinkLiteralNode, + ComlinkNoneLiteralNode, ComlinkObjectLiteralNode, ComlinkPrimitiveLiteralNode, EnumDefinitionNode, @@ -40,6 +41,7 @@ import { } from '.'; import { validateListLiteral, + validateNoneLiteral, validateObjectLiteral, validatePrimitiveLiteral, } from './utils'; @@ -102,6 +104,8 @@ export class ExampleValidator implements ProfileAstVisitor { return this.visitComlinkObjectLiteralNode(node); case 'ComlinkAssignment': return this.visitComlinkAssignmentNode(node); + case 'ComlinkNoneLiteral': + return this.visitComlinkNoneLiteralNode(node); // UNUSED case 'FieldDefinition': return this.visitFieldDefinitionNode(node); @@ -229,6 +233,25 @@ export class ExampleValidator implements ProfileAstVisitor { return isValid; } + visitComlinkNoneLiteralNode(node: ComlinkNoneLiteralNode): boolean { + assertDefinedStructure(this.currentStructure); + + const { isValid } = validateNoneLiteral(this.currentStructure); + + if (!isValid) { + this.errors.push({ + kind: 'wrongStructure', + context: { + path: this.getPath(node), + expected: this.currentStructure, + actual: node, + }, + }); + } + + return isValid; + } + visitComlinkListLiteralNode(node: ComlinkListLiteralNode): boolean { if (this.structureIsPrepared(node)) { return true; diff --git a/src/interpreter/profile-io-analyzer.ts b/src/interpreter/profile-io-analyzer.ts index 869530c7..8fbc3a9c 100644 --- a/src/interpreter/profile-io-analyzer.ts +++ b/src/interpreter/profile-io-analyzer.ts @@ -1,6 +1,7 @@ import { ComlinkAssignmentNode, ComlinkListLiteralNode, + ComlinkNoneLiteralNode, ComlinkObjectLiteralNode, ComlinkPrimitiveLiteralNode, DocumentedNode, @@ -141,6 +142,8 @@ export class ProfileIOAnalyzer implements ProfileAstVisitor { return this.visitComlinkObjectLiteralNode(node); case 'ComlinkAssignment': return this.visitComlinkAssignmentNode(node); + case 'ComlinkNoneLiteral': + return this.visitComlinkNoneLiteralNode(node); default: assertUnreachable(node); } @@ -328,6 +331,10 @@ export class ProfileIOAnalyzer implements ProfileAstVisitor { throw new Error('Not Implemented'); } + visitComlinkNoneLiteralNode(_node: ComlinkNoneLiteralNode): void { + throw new Error('Not Implemented'); + } + /** * store the named fields for later reference */ diff --git a/src/interpreter/utils.ts b/src/interpreter/utils.ts index 2b906a8d..d73c86f4 100644 --- a/src/interpreter/utils.ts +++ b/src/interpreter/utils.ts @@ -94,6 +94,8 @@ function formatLiteral( case 'PrimitiveLiteral': case 'ComlinkPrimitiveLiteral': return formatPrimitive(literal.value); + case 'ComlinkNoneLiteral': + return 'None'; case 'ObjectLiteral': case 'ComlinkObjectLiteral': return `{${literal.fields.map(formatLiteral).join(', ')}}`; @@ -269,6 +271,14 @@ export function validateListLiteral( return { isValid: false }; } +export function validateNoneLiteral(structure: StructureType): { isValid: boolean } { + if (structure.kind === 'NonNullStructure') { + return { isValid: false }; + } + + return { isValid: true } +} + export function getOutcomes( node: MapDefinitionNode | OperationDefinitionNode, isErrorFilter?: boolean diff --git a/src/language/language.test.ts b/src/language/language.test.ts index 6a872bed..55ce58f0 100644 --- a/src/language/language.test.ts +++ b/src/language/language.test.ts @@ -467,7 +467,10 @@ describe('profile', () => { it('should parse profile with examples', () => { const input = ` usecase Foo { - input { f! string! } + input { + f! string! + fn string + } result number error enum { FORBIDDEN_WORD @@ -478,6 +481,7 @@ describe('profile', () => { input { "hello has 5 letters" f = "hello" + fn = None } result 5 // TODO: do we want this? async result undefined @@ -531,6 +535,13 @@ describe('profile', () => { title: 'hello has 5 letters', }, }, + { + kind: 'ComlinkAssignment', + key: ['fn'], + value: { + kind: 'ComlinkNoneLiteral' + } + } ], }, }, diff --git a/src/language/lexer/lexer.test.ts b/src/language/lexer/lexer.test.ts index 00b56f87..b11cda18 100644 --- a/src/language/lexer/lexer.test.ts +++ b/src/language/lexer/lexer.test.ts @@ -348,7 +348,7 @@ describe('lexer', () => { test('identifiers', () => { const lexer = new Lexer( new Source( - 'ident my fier pls usecaseNOT modelout boolean b00lean a123456789_0' + 'ident my fier pls usecaseNOT modelout boolean b00lean a123456789_0 None' ) ); const expectedTokens: (LexerTokenData | IdentifierValue)[] = [ @@ -362,6 +362,7 @@ describe('lexer', () => { 'boolean', 'b00lean', 'a123456789_0', + 'None', { kind: LexerTokenKind.SEPARATOR, separator: 'EOF' }, ]; diff --git a/src/language/lexer/sublexer/jessie/expression.ts b/src/language/lexer/sublexer/jessie/expression.ts index 8c59b5cb..815f64ff 100644 --- a/src/language/lexer/sublexer/jessie/expression.ts +++ b/src/language/lexer/sublexer/jessie/expression.ts @@ -864,7 +864,7 @@ function resolveTerminationTokens( // Tokens that are always terminator tokens // What isn't included here is the ts.SyntaxKind.EndOfFileToken token - this token is hardcoded into the scanner because it ignores nesting - if (!(ts.SyntaxKind.SingleLineCommentTrivia in termTokens)) { + if (!termTokens.includes(ts.SyntaxKind.SingleLineCommentTrivia)) { termTokens.push(ts.SyntaxKind.SingleLineCommentTrivia); } diff --git a/src/language/lexer/token.ts b/src/language/lexer/token.ts index eed36589..dc21ed89 100644 --- a/src/language/lexer/token.ts +++ b/src/language/lexer/token.ts @@ -179,7 +179,7 @@ export class LexerToken { /** Data of the token. */ readonly data: LexerTokenData, readonly location: LocationSpan - ) {} + ) { } isSOF(): boolean { return ( diff --git a/src/language/syntax/rules/profile/literal.ts b/src/language/syntax/rules/profile/literal.ts index 2223b28e..955055dd 100644 --- a/src/language/syntax/rules/profile/literal.ts +++ b/src/language/syntax/rules/profile/literal.ts @@ -2,6 +2,7 @@ import { ComlinkAssignmentNode, ComlinkListLiteralNode, ComlinkLiteralNode, + ComlinkNoneLiteralNode, ComlinkObjectLiteralNode, ComlinkPrimitiveLiteralNode, } from '@superfaceai/ast'; @@ -90,8 +91,20 @@ export const COMLINK_LIST_LITERAL: SyntaxRule< } ); +export const COMLINK_NONE_LITERAL: SyntaxRule< + WithLocation +> = SyntaxRule.identifier('None').map( + (match): WithLocation => { + return { + kind: 'ComlinkNoneLiteral', + location: match.location, + }; + } +); + export const COMLINK_LITERAL = SyntaxRule.or( COMLINK_PRIMITIVE_LITERAL, + COMLINK_NONE_LITERAL, COMLINK_OBJECT_LITERAL, COMLINK_LIST_LITERAL ); diff --git a/src/language/syntax/rules/profile/profile.test.ts b/src/language/syntax/rules/profile/profile.test.ts index 826e39c9..e6c3219c 100644 --- a/src/language/syntax/rules/profile/profile.test.ts +++ b/src/language/syntax/rules/profile/profile.test.ts @@ -1484,7 +1484,7 @@ describe('profile syntax rules', () => { tesTok({ kind: LexerTokenKind.IDENTIFIER, identifier: 'version' }), tesTok({ kind: LexerTokenKind.OPERATOR, operator: '=' }), - tesTok({ kind: LexerTokenKind.STRING, string: '11.12' }), + tesTok({ kind: LexerTokenKind.STRING, string: '11.12.3' }), ]; const stream = new ArrayLexerStream(tokens); @@ -1498,7 +1498,7 @@ describe('profile syntax rules', () => { version: { major: 11, minor: 12, - patch: 0, + patch: 3, }, documentation: { title: 'Title', @@ -1522,7 +1522,7 @@ describe('profile syntax rules', () => { tesTok({ kind: LexerTokenKind.IDENTIFIER, identifier: 'version' }), tesTok({ kind: LexerTokenKind.OPERATOR, operator: '=' }), - tesTok({ kind: LexerTokenKind.STRING, string: '1' }), + tesTok({ kind: LexerTokenKind.STRING, string: '1.2.3' }), tesTok({ kind: LexerTokenKind.IDENTIFIER, identifier: 'model' }), tesTok({ kind: LexerTokenKind.IDENTIFIER, identifier: 'model1' }), @@ -1559,8 +1559,8 @@ describe('profile syntax rules', () => { name: 'profile', version: { major: 1, - minor: 0, - patch: 0, + minor: 2, + patch: 3, }, }, tokens[1], @@ -1630,6 +1630,23 @@ describe('profile syntax rules', () => { ) ); }); + + it('should require full version in profile header', () => { + const tokens: ReadonlyArray = [ + tesTok({ kind: LexerTokenKind.IDENTIFIER, identifier: 'name' }), + tesTok({ kind: LexerTokenKind.OPERATOR, operator: '=' }), + tesTok({ kind: LexerTokenKind.STRING, string: 'scope/profile' }), + + tesTok({ kind: LexerTokenKind.IDENTIFIER, identifier: 'version' }), + tesTok({ kind: LexerTokenKind.OPERATOR, operator: '=' }), + tesTok({ kind: LexerTokenKind.STRING, string: '1.2' }), + ]; + const stream = new ArrayLexerStream(tokens); + + const rule = rules.PROFILE_HEADER; + + expect(rule.tryMatch(stream)).not.toBeAMatch(); + }); }); describe('comlink literals', () => { @@ -1652,6 +1669,24 @@ describe('profile syntax rules', () => { ); }); + it('should parse none literal', () => { + const tokens: ReadonlyArray = [ + tesTok({ kind: LexerTokenKind.IDENTIFIER, identifier: 'None' }), + ]; + const stream = new ArrayLexerStream(tokens); + + const rule = rules.COMLINK_NONE_LITERAL; + + expect(rule.tryMatch(stream)).toBeAMatch( + tesMatch( + { + kind: 'ComlinkNoneLiteral', + }, + tokens[0] + ) + ); + }); + it('should parse object literal', () => { const tokens: ReadonlyArray = [ tesTok({ kind: LexerTokenKind.SEPARATOR, separator: '{' }), diff --git a/src/language/syntax/rules/profile/profile.ts b/src/language/syntax/rules/profile/profile.ts index 05e1c753..a7b3a118 100644 --- a/src/language/syntax/rules/profile/profile.ts +++ b/src/language/syntax/rules/profile/profile.ts @@ -22,8 +22,8 @@ import { UseCaseSlotDefinitionNode, } from '@superfaceai/ast'; +import { ProfileVersion } from '../../../../common'; import { parseDocumentId } from '../../../../common/document/parser'; -import { VersionRange } from '../../../../common/document/version'; import { PARSED_AST_VERSION, PARSED_VERSION } from '../../../../metadata'; import { IdentifierTokenData, LexerTokenKind } from '../../../lexer/token'; import { @@ -568,14 +568,14 @@ const PROFILE_VERSION = SyntaxRule.followedBy( } & HasLocation >(version => { try { - const parsedVersion = VersionRange.fromString(version.data.string); + const parsedVersion = ProfileVersion.fromString(version.data.string); return { kind: 'match', value: { major: parsedVersion.major, - minor: parsedVersion.minor ?? 0, - patch: parsedVersion.patch ?? 0, + minor: parsedVersion.minor, + patch: parsedVersion.patch, label: parsedVersion.label, location: version.location, }, @@ -583,7 +583,7 @@ const PROFILE_VERSION = SyntaxRule.followedBy( } catch (error) { return { kind: 'nomatch' }; } - }, 'semver version') + }, 'semver version in format `..`') ).map(([keyword, op, version]) => { return { version: { diff --git a/yarn.lock b/yarn.lock index 43d1c432..387d6433 100644 --- a/yarn.lock +++ b/yarn.lock @@ -566,10 +566,10 @@ dependencies: "@sinonjs/commons" "^1.7.0" -"@superfaceai/ast@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@superfaceai/ast/-/ast-1.2.0.tgz#f41823d550e0b0a05a8398be22a68b7b3912dde3" - integrity sha512-HMmdSUNzKuVgQ9g9YS/eiv3/CitSZks8eury8sAi2QbwT+n5f9SsVcmHxL2/QbkRCmlPpRqHJ6YSCqvBZ2fu1g== +"@superfaceai/ast@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@superfaceai/ast/-/ast-1.3.0.tgz#15b8ef512616652478592b2469ba06303b713622" + integrity sha512-6zTaJKaZkR5kqk3axiE05FheRWYb0ykRSMy6quwrVn4fJmP/Z6KQ14KifqMtZTmWak+GsFPfun1n1reyGyHbJg== dependencies: ajv "^8.8.2" ajv-formats "^2.1.1" @@ -2611,9 +2611,9 @@ json5@2.x, json5@^2.2.1: integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" @@ -2747,9 +2747,9 @@ minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: brace-expansion "^1.1.7" minimist@^1.2.0, minimist@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== ms@2.0.0: version "2.0.0"