diff --git a/src/validator.ts b/src/validator.ts index cf83361..5c2bc52 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -8,7 +8,10 @@ import { RecognitionException, } from 'antlr4ts'; import { YorkieSchemaLexer } from '../antlr/YorkieSchemaLexer'; -import { YorkieSchemaParser } from '../antlr/YorkieSchemaParser'; +import { + PropertyNameContext, + YorkieSchemaParser, +} from '../antlr/YorkieSchemaParser'; import { YorkieSchemaListener } from '../antlr/YorkieSchemaListener'; import { TypeAliasDeclarationContext, @@ -16,12 +19,18 @@ import { } from '../antlr/YorkieSchemaParser'; import { ParseTreeWalker } from 'antlr4ts/tree'; +/** + * `TypeSymbol` represents a type alias declaration. + */ type TypeSymbol = { name: string; line: number; column: number; }; +/** + * `TypeReference` represents a type reference in a type alias declaration. + */ type TypeReference = { name: string; parent: string; @@ -29,22 +38,60 @@ type TypeReference = { column: number; }; +/** + * `Diagnostic` represents a diagnostic message. + */ +export type Diagnostic = { + severity: 'error' | 'warning' | 'info'; + message: string; + range: { + start: { column: number; line: number }; + end: { column: number; line: number }; + }; +}; + export class TypeCollectorListener implements YorkieSchemaListener { - public symbolTable: Map = new Map(); - public errors: Array<{ message: string; line: number; column: number }> = []; + private symbol: string | null = null; + private properties: Set = new Set(); - public parent: string | null = null; + public symbolMap: Map = new Map(); public referenceMap: Map = new Map(); + public errors: Array<{ message: string; line: number; column: number }> = []; enterTypeAliasDeclaration(ctx: TypeAliasDeclarationContext) { const typeName = ctx.Identifier().text; const { line, charPositionInLine } = ctx.Identifier().symbol; - this.symbolTable.set(typeName, { + + if (this.symbolMap.has(typeName)) { + this.errors.push({ + message: `Duplicate type declaration: ${typeName}`, + line: line, + column: charPositionInLine, + }); + } + + this.symbolMap.set(typeName, { name: typeName, line: line, column: charPositionInLine, }); - this.parent = typeName; + + this.symbol = typeName; + } + + enterPropertyName(ctx: PropertyNameContext) { + const typeName = ctx.Identifier()!.text; + const { line, charPositionInLine } = ctx.Identifier()!.symbol; + + if (this.properties.has(typeName)) { + this.errors.push({ + message: `Duplicate property name: ${typeName}`, + line: line, + column: charPositionInLine, + }); + } + + this.properties.add(typeName); } enterTypeReference(ctx: TypeReferenceContext) { @@ -53,22 +100,13 @@ export class TypeCollectorListener implements YorkieSchemaListener { this.referenceMap.set(typeName, { name: typeName, - parent: this.parent!, + parent: this.symbol!, line: line, column: charPositionInLine, }); } } -export type Diagnostic = { - severity: 'error' | 'warning' | 'info'; - message: string; - range: { - start: { column: number; line: number }; - end: { column: number; line: number }; - }; -}; - class LexerErrorListener implements ANTLRErrorListener { constructor(private errorList: Diagnostic[]) {} @@ -144,7 +182,7 @@ export function validate(data: string): { errors: Array } { // TODO(hackerwins): This is a naive implementation and performance can be improved. for (const [, ref] of listener.referenceMap) { - if (!listener.symbolTable.has(ref.name)) { + if (!listener.symbolMap.has(ref.name)) { listener.errors.push({ message: `Type '${ref.name}' is not defined.`, line: ref.line, @@ -153,7 +191,7 @@ export function validate(data: string): { errors: Array } { } } - for (const [, symbol] of listener.symbolTable) { + for (const [, symbol] of listener.symbolMap) { const visited = new Set(); let current: string | undefined = symbol.name; while (current) { diff --git a/test/schema.test.ts b/test/schema.test.ts index 017e9c7..79b7f4a 100644 --- a/test/schema.test.ts +++ b/test/schema.test.ts @@ -97,6 +97,26 @@ describe('Schema:TypeScript', () => { `; expect(validate(schema).errors.length).toBe(0); }); + + it('should detect duplicate type alias declarations', () => { + const schema = ` + type Document = { + }; + type Document = { + }; + `; + expect(validate(schema).errors.length).toBeGreaterThan(0); + }); + + it('should detect duplicate keys in object', () => { + const schema = ` + type Document = { + field: string; + field: number; + }; + `; + expect(validate(schema).errors.length).toBeGreaterThan(0); + }); }); describe('Schema:Yorkie', () => {